improvement!: Update to support AshAuthentication 3.x (#27)

This commit is contained in:
James Harton 2022-12-08 14:32:26 +13:00 committed by GitHub
parent d7c097f10d
commit 1d3e4e4641
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 552 additions and 586 deletions

View file

@ -27,9 +27,13 @@ config :ash_authentication, AshAuthentication.Jwt,
config :phoenix, :json_library, Jason
config :ash_authentication_phoenix, Example.Accounts.User,
oauth2_authentication: [
client_id: System.get_env("OAUTH2_CLIENT_ID"),
client_secret: System.get_env("OAUTH2_CLIENT_SECRET"),
redirect_uri: "http://localhost:4000/auth",
site: System.get_env("OAUTH2_SITE")
authentication: [
strategies: [
auth0: [
client_id: System.get_env("OAUTH2_CLIENT_ID"),
client_secret: System.get_env("OAUTH2_CLIENT_SECRET"),
redirect_uri: "http://localhost:4000/auth",
site: System.get_env("OAUTH2_SITE")
]
]
]

View file

@ -1,6 +1,4 @@
defmodule Dev.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
@ -8,22 +6,15 @@ defmodule Dev.Application do
@impl true
def start(_type, _args) do
children = [
# Start the PubSub system
{Phoenix.PubSub, name: Dev.PubSub},
# Start the Endpoint (http/https)
DevWeb.Endpoint
# Start a worker by calling: Dev.Worker.start_link(arg)
# {Dev.Worker, arg}
DevWeb.Endpoint,
{AshAuthentication.Supervisor, otp_app: :ash_authentication_phoenix}
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Dev.Supervisor]
Supervisor.start_link(children, opts)
end
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
@impl true
def config_change(changed, _new, removed) do
DevWeb.Endpoint.config_change(changed, removed)

View file

@ -6,7 +6,7 @@ defmodule DevWeb.AuthController do
@doc false
@impl true
def success(conn, user, _token) do
def success(conn, _activity, user, _token) do
conn
|> store_in_session(user)
|> assign(:current_user, user)
@ -16,7 +16,7 @@ defmodule DevWeb.AuthController do
@doc false
@impl true
def failure(conn, reason) do
def failure(conn, _activity, reason) do
conn
|> assign(:failure_reason, reason)
|> put_status(401)

View file

@ -2,7 +2,7 @@ defmodule DevWeb.Router do
@moduledoc false
use DevWeb, :router
use AshAuthentication.Phoenix.Router
use AshAuthentication.Phoenix.Router, otp_app: :ash_authentication_phoenix
pipeline :browser do
plug :accepts, ["html"]
@ -31,7 +31,7 @@ defmodule DevWeb.Router do
scope "/", DevWeb do
pipe_through :browser
auth_routes(AuthController, "/auth")
auth_routes_for(Example.Accounts.User, to: AuthController, path: "/auth")
sign_in_route("/sign-in")
sign_out_route(AuthController, "/sign-out")
end

View file

@ -1,54 +0,0 @@
defmodule AshAuthentication.Phoenix.AuthenticationComponent do
@moduledoc """
A basic behaviour to help us lay our components out on the screen.
## Usage
```elixir
defmodule MyComponent do
use AshAuthentication.Phoenix.AuthenticationComponent, style: :link
end
```
We have two "styles" of component, `:form` and `:link`, which defines how they
should be rendered before or after the "or" divider.
You will need to implement this if you're using
`AshAuthentication.Phoenix.Components.SignIn` for your component to be
visible. You can ignore if it you're not.
"""
@doc """
Is the component a `:link` style?
Auto-generated, but overridable.
"""
@callback link? :: boolean
@doc """
Is the component a `:form` style?
Auto-generated, but overridable.
"""
@callback form? :: boolean
@type options :: [{:style, :link | :form}]
@doc false
@spec __using__(options) :: Macro.t()
defmacro __using__(opts) do
style = Keyword.get(opts, :style)
quote do
@behaviour AshAuthentication.Phoenix.AuthenticationComponent
@doc false
@spec link? :: boolean
def link?, do: unquote(style == :link)
@doc false
@spec form? :: boolean
def form?, do: unquote(style == :form)
end
end
end

View file

@ -11,6 +11,8 @@ defmodule AshAuthentication.Phoenix.Components.Banner do
@moduledoc """
Renders a very simple banner at the top of the sign-in component.
Can show either an image or some text, depending on the provided overrides.
#{AshAuthentication.Phoenix.Overrides.Overridable.generate_docs()}
"""

View file

@ -1,4 +1,4 @@
defmodule AshAuthentication.Phoenix.Components.OAuth2Authentication do
defmodule AshAuthentication.Phoenix.Components.OAuth2 do
use AshAuthentication.Phoenix.Overrides.Overridable,
root_class: "CSS classes for the root `div` element.",
link_class: "CSS classes for the `a` element."
@ -13,15 +13,15 @@ defmodule AshAuthentication.Phoenix.Components.OAuth2Authentication do
## Props
* `provider` - The provider module.
* `config` - The configuration as per
`AshAuthentication.authenticated_resources/1`. Required.
* `strategy` - The strategy configuration as per
`AshAuthentication.Info.strategy/2`. Required.
* `socket` - Needed to infer the otp-app from the Phoenix endpoint.
#{AshAuthentication.Phoenix.Overrides.Overridable.generate_docs()}
"""
use Phoenix.LiveComponent
use AshAuthentication.Phoenix.AuthenticationComponent, style: :link
alias AshAuthentication.Info
alias Phoenix.LiveView.{Rendered, Socket}
import AshAuthentication.Phoenix.Components.Helpers, only: [route_helpers: 1]
import Phoenix.HTML.Form
@ -29,30 +29,30 @@ defmodule AshAuthentication.Phoenix.Components.OAuth2Authentication do
@doc false
@spec render(Socket.assigns()) :: Rendered.t() | no_return
def render(assigns) do
assigns =
assigns
|> assign(:subject_name, Info.authentication_subject_name!(assigns.strategy.resource))
~H"""
<div class={override_for(@socket, :root_class)}>
<a
href={
route_helpers(@socket).auth_request_path(
route_helpers(@socket).auth_path(
@socket.endpoint,
:request,
@config.subject_name,
@provider.provides(@config.resource)
{@subject_name, @strategy.name, :request}
)
}
class={override_for(@socket, :link_class)}
>
Sign in with <%= provider_name(@provider, @config) %>
Sign in with <%= strategy_name(@strategy) %>
</a>
</div>
"""
end
defp provider_name(provider, config) do
config.resource
|> provider.provides()
|> case do
"oauth2" -> "OAuth"
defp strategy_name(strategy) do
case strategy.name do
:oauth2 -> "OAuth"
other -> humanize(other)
end
end

View file

@ -1,4 +1,4 @@
defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication do
defmodule AshAuthentication.Phoenix.Components.Password do
use AshAuthentication.Phoenix.Overrides.Overridable,
root_class: "CSS class for the root `div` element.",
hide_class: "CSS class to apply to hide an element.",
@ -10,7 +10,7 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication do
toggler_class: "CSS class for the toggler `a` element."
@moduledoc """
Generates sign in and registration forms for a resource.
Generates sign in, registration and reset forms for a resource.
## Component hierarchy
@ -19,67 +19,71 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication do
Children:
* `AshAuthentication.Phoenix.Components.PasswordAuthentication.SignInForm`
* `AshAuthentication.Phoenix.Components.PasswordAuthentication.RegisterForm`
* `AshAuthentication.Phoenix.Components.PasswordAuthentication.ResetForm`
* `AshAuthentication.Phoenix.Components.Password.SignInForm`
* `AshAuthentication.Phoenix.Components.Password.RegisterForm`
* `AshAuthentication.Phoenix.Components.Password.ResetForm`
## Props
* `config` - The configuration as per
`AshAuthentication.authenticated_resources/1`. Required.
* `strategy` - The strategy configuration as per
`AshAuthentication.Info.strategy/2`. Required.
* `socket` - Needed to infer the otp-app from the Phoenix endpoint.
#{AshAuthentication.Phoenix.Overrides.Overridable.generate_docs()}
"""
use Phoenix.LiveComponent
use AshAuthentication.Phoenix.AuthenticationComponent, style: :form
alias __MODULE__
alias AshAuthentication.{PasswordAuthentication.Info, PasswordReset}
alias AshAuthentication.{Info, Phoenix.Components.Password}
alias Phoenix.LiveView.{JS, Rendered, Socket}
import Slug
@doc false
@spec render(Socket.assigns()) :: Rendered.t() | no_return
def render(assigns) do
config = assigns.config
provider = assigns.provider
sign_in_action = Info.password_authentication_sign_in_action_name!(assigns.config.resource)
register_action = Info.password_authentication_register_action_name!(assigns.config.resource)
reset_enabled? = PasswordReset.enabled?(assigns.config.resource)
strategy = assigns.strategy
reset_action =
if reset_enabled?,
do: PasswordReset.Info.request_password_reset_action_name!(assigns.config.resource)
subject_name =
assigns.strategy.resource
|> Info.authentication_get_by_subject_action_name!()
|> to_string()
|> slugify()
strategy_name =
assigns.strategy.name
|> to_string()
|> slugify()
reset_enabled? = Enum.any?(strategy.resettable)
reset_id =
strategy.resettable
|> Enum.map(
&generate_id(subject_name, strategy_name, &1.request_password_reset_action_name)
)
|> List.first()
assigns =
assigns
|> assign(:sign_in_action, sign_in_action)
|> assign_new(:sign_in_id, fn ->
"#{config.subject_name}_#{provider.provides(config.resource)}_#{sign_in_action}"
end)
|> assign(:register_action, register_action)
|> assign_new(:register_id, fn ->
"#{config.subject_name}_#{provider.provides(config.resource)}_#{register_action}"
end)
|> assign_new(:show_first, fn ->
override_for(assigns.socket, :show_first, :sign_in)
end)
|> assign_new(:hide_class, fn ->
override_for(assigns.socket, :hide_class)
end)
|> assign(
:sign_in_id,
generate_id(subject_name, strategy_name, strategy.sign_in_action_name)
)
|> assign(
:register_id,
generate_id(subject_name, strategy_name, strategy.register_action_name)
)
|> assign(:show_first, override_for(assigns.socket, :show_first, :sign_in))
|> assign(:hide_class, override_for(assigns.socket, :hide_class))
|> assign(:reset_enabled?, reset_enabled?)
|> assign_new(:reset_id, fn ->
if reset_enabled?,
do: "#{config.subject_name}_#{provider.provides(config.resource)}_#{reset_action}"
end)
|> assign(:reset_id, reset_id)
~H"""
<div class={override_for(@socket, :root_class)}>
<div id={"#{@sign_in_id}-wrapper"} class={unless @show_first == :sign_in, do: @hide_class}>
<.live_component
module={PasswordAuthentication.SignInForm}
module={Password.SignInForm}
id={@sign_in_id}
provider={@provider}
config={@config}
strategy={@strategy}
label={false}
>
<div class={override_for(@socket, :interstitial_class)}>
@ -101,12 +105,12 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication do
</div>
</.live_component>
</div>
<div id={"#{@register_id}-wrapper"} class={unless @show_first == :register, do: @hide_class}>
<.live_component
module={PasswordAuthentication.RegisterForm}
module={Password.RegisterForm}
id={@register_id}
provider={@provider}
config={@config}
strategy={@strategy}
label={false}
>
<div class={override_for(@socket, :interstitial_class)}>
@ -118,7 +122,6 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication do
message={override_for(@socket, :reset_toggle_text)}
/>
<% end %>
<.toggler
socket={@socket}
show={@sign_in_id}
@ -128,13 +131,13 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication do
</div>
</.live_component>
</div>
<%= if @reset_enabled? do %>
<div id={"#{@reset_id}-wrapper"} class={unless @show_first == :reset, do: @hide_class}>
<.live_component
module={PasswordAuthentication.ResetForm}
module={Password.ResetForm}
id={@reset_id}
provider={@provider}
config={@config}
strategy={@strategy}
label={false}
>
<div class={override_for(@socket, :interstitial_class)}>
@ -144,7 +147,6 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication do
hide={[@sign_in_id, @reset_id]}
message={override_for(@socket, :register_toggle_text)}
/>
<.toggler
socket={@socket}
show={@sign_in_id}
@ -159,6 +161,15 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication do
"""
end
defp generate_id(subject_name, strategy_name, action) do
action =
action
|> to_string()
|> slugify()
"#{subject_name}-#{strategy_name}-#{action}"
end
@doc false
@spec toggler(Socket.assigns()) :: Rendered.t() | no_return
def toggler(assigns) do

View file

@ -1,4 +1,4 @@
defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
defmodule AshAuthentication.Phoenix.Components.Password.Input do
use AshAuthentication.Phoenix.Overrides.Overridable,
field_class: "CSS class for `div` elements surrounding the fields.",
label_class: "CSS class for `label` elements.",
@ -11,19 +11,21 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
input_debounce: "Number of milliseconds to debounce input by (or `nil` to disable)."
@moduledoc """
Function components for dealing with form input during password authentication.
Function components for dealing with form input during password
authentication.
## Component hierarchy
These function components are consumed by
`AshAuthentication.Phoenix.Components.PasswordAuthentication.SignInForm` and
`AshAuthentication.Phoenix.Components.PasswordAuthentication.RegisterForm`.
`AshAuthentication.Phoenix.Components.Password.SignInForm`,
`AshAuthentication.Phoenix.Components.Password.RegisterForm` and
`AshAuthentication.Phoenix.Components.ResetForm`.
#{AshAuthentication.Phoenix.Overrides.Overridable.generate_docs()}
"""
use Phoenix.Component
alias AshAuthentication.{PasswordAuthentication, PasswordReset}
alias AshAuthentication.Strategy
alias AshPhoenix.Form
alias Phoenix.LiveView.{Rendered, Socket}
import Phoenix.HTML.Form
@ -36,7 +38,7 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
* `socket` - Phoenix LiveView socket.
This is needed to be able to retrieve the correct CSS configuration.
Required.
* `config` - The configuration map as per
* `strategy` - The configuration map as per
`AshAuthentication.authenticated_resources/1`.
Required.
* `form` - An `AshPhoenix.Form`.
@ -46,13 +48,12 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
"""
@spec identity_field(%{
required(:socket) => Socket.t(),
required(:config) => AshAuthentication.resource_config(),
required(:form) => AshPhoenix.Form.t(),
required(:strategy) => Strategy.t(),
required(:form) => Form.t(),
optional(:input_type) => :text | :email
}) :: Rendered.t() | no_return
def identity_field(assigns) do
identity_field =
PasswordAuthentication.Info.password_authentication_identity_field!(assigns.config.resource)
identity_field = assigns.strategy.identity_field
assigns =
assigns
@ -60,7 +61,7 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
|> assign_new(:input_type, fn ->
identity_field
|> to_string()
|> String.starts_with?("email")
|> String.contains?("email")
|> then(fn
true -> :email
_ -> :text
@ -95,7 +96,7 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
* `socket` - Phoenix LiveView socket.
This is needed to be able to retrieve the correct CSS configuration.
Required.
* `config` - The configuration map as per
* `strategy` - The configuration map as per
`AshAuthentication.authenticated_resources/1`.
Required.
* `form` - An `AshPhoenix.Form`.
@ -103,12 +104,11 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
"""
@spec password_field(%{
required(:socket) => Socket.t(),
required(:config) => AshAuthentication.resource_config(),
required(:form) => AshPhoenix.Form.t()
required(:strategy) => Strategy.t(),
required(:form) => Form.t()
}) :: Rendered.t() | no_return
def password_field(assigns) do
password_field =
PasswordAuthentication.Info.password_authentication_password_field!(assigns.config.resource)
password_field = assigns.strategy.password_field
assigns =
assigns
@ -142,7 +142,7 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
* `socket` - Phoenix LiveView socket.
This is needed to be able to retrieve the correct CSS configuration.
Required.
* `config` - The configuration map as per
* `strategy` - The configuration map as per
`AshAuthentication.authenticated_resources/1`.
Required.
* `form` - An `AshPhoenix.Form`.
@ -150,14 +150,11 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
"""
@spec password_confirmation_field(%{
required(:socket) => Socket.t(),
required(:config) => AshAuthentication.resource_config(),
required(:form) => AshPhoenix.Form.t()
required(:strategy) => Strategy.t(),
required(:form) => Form.t()
}) :: Rendered.t() | no_return
def password_confirmation_field(assigns) do
password_confirmation_field =
PasswordAuthentication.Info.password_authentication_password_confirmation_field!(
assigns.config.resource
)
password_confirmation_field = assigns.strategy.password_confirmation_field
assigns =
assigns
@ -191,7 +188,7 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
* `socket` - Phoenix LiveView socket.
This is needed to be able to retrieve the correct CSS configuration.
Required.
* `config` - The configuration map as per
* `strategy` - The configuration map as per
`AshAuthentication.authenticated_resources/1`.
Required.
* `form` - An `AshPhoenix.Form`.
@ -204,8 +201,8 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
"""
@spec submit(%{
required(:socket) => Socket.t(),
required(:config) => AshAuthentication.resource_config(),
required(:form) => AshPhoenix.Form.t(),
required(:strategy) => Strategy.t(),
required(:form) => Form.t(),
required(:action) => :sign_in | :register,
optional(:label) => String.t()
}) :: Rendered.t() | no_return
@ -215,16 +212,15 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
|> assign_new(:label, fn ->
case assigns.action do
:request_reset ->
assigns.config.resource
|> PasswordReset.Info.request_password_reset_action_name!()
assigns.strategy.resettable
|> Enum.map(& &1.request_password_reset_action_name)
|> List.first() || :reqest_reset
:sign_in ->
assigns.config.resource
|> PasswordAuthentication.Info.password_authentication_sign_in_action_name!()
assigns.strategy.sign_in_action_name
:register ->
assigns.config.resource
|> PasswordAuthentication.Info.password_authentication_register_action_name!()
assigns.strategy.register_action_name
end
|> humanize()
end)

View file

@ -1,4 +1,4 @@
defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.RegisterForm do
defmodule AshAuthentication.Phoenix.Components.Password.RegisterForm do
use AshAuthentication.Phoenix.Overrides.Overridable,
root_class: "CSS class for the root `div` element.",
label_class: "CSS class for the `h2` element.",
@ -11,65 +11,61 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.RegisterFo
## Component hierarchy
This is a child of `AshAuthentication.Phoenix.Components.PasswordAuthentication`.
This is a child of `AshAuthentication.Phoenix.Components.Password`.
Children:
* `AshAuthentication.Phoenix.Components.PasswordAuthentication.Input.identity_field/1`
* `AshAuthentication.Phoenix.Components.PasswordAuthentication.Input.password_field/1`
* `AshAuthentication.Phoenix.Components.PasswordAuthentication.Input.password_confirmation_field/1`
* `AshAuthentication.Phoenix.Components.PasswordAuthentication.Input.submit/1`
* `AshAuthentication.Phoenix.Components.Password.Input.identity_field/1`
* `AshAuthentication.Phoenix.Components.Password.Input.password_field/1`
* `AshAuthentication.Phoenix.Components.Password.Input.password_confirmation_field/1`
* `AshAuthentication.Phoenix.Components.Password.Input.submit/1`
## Props
* `config` - The configuration map as per
`AshAuthentication.authenticated_resources/1`.
Required.
* `label` - The text to show in the submit label.
Generated from the configured action name (via
`Phoenix.HTML.Form.humanize/1`) if not supplied.
Set to `false` to disable.
* `strategy` - The strategy configuration as per
`AshAuthentication.Info.strategy/2`. Required.
* `socket` - Needed to infer the otp-app from the Phoenix endpoint.
#{AshAuthentication.Phoenix.Overrides.Overridable.generate_docs()}
"""
use Phoenix.LiveComponent
alias AshAuthentication.{
PasswordAuthentication,
Phoenix.Components.PasswordAuthentication.Input
}
alias AshAuthentication.{Info, Phoenix.Components.Password.Input}
alias AshPhoenix.Form
alias Phoenix.LiveView.{Rendered, Socket}
import Phoenix.HTML.Form
import AshAuthentication.Phoenix.Components.Helpers
import Slug
@doc false
@impl true
@spec update(Socket.assigns(), Socket.t()) :: {:ok, Socket.t()}
def update(assigns, socket) do
config = assigns.config
strategy = assigns.strategy
action =
PasswordAuthentication.Info.password_authentication_register_action_name!(config.resource)
confirm? =
PasswordAuthentication.Info.password_authentication_confirmation_required?(config.resource)
api = Info.authentication_api!(strategy.resource)
subject_name = Info.authentication_subject_name!(strategy.resource)
form =
config.resource
|> Form.for_action(action,
api: config.api,
as: to_string(config.subject_name),
id: "#{PasswordAuthentication.provides(config.resource)}_#{config.subject_name}_#{action}"
strategy.resource
|> Form.for_action(strategy.register_action_name,
api: api,
as: subject_name |> to_string(),
id: "#{subject_name}-#{strategy.name}-#{strategy.register_action_name}" |> slugify(),
context: %{strategy: strategy}
)
socket =
socket
|> assign(assigns)
|> assign(form: form, trigger_action: false, confirm?: confirm?)
|> assign_new(:label, fn -> humanize(action) end)
|> assign(
form: form,
trigger_action: false,
subject_name: subject_name
)
|> assign_new(:label, fn -> humanize(strategy.register_action_name) end)
|> assign_new(:inner_block, fn -> nil end)
{:ok, socket}
@ -95,23 +91,19 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.RegisterFo
phx-trigger-action={@trigger_action}
phx-target={@myself}
action={
route_helpers(@socket).auth_callback_path(
route_helpers(@socket).auth_path(
@socket.endpoint,
:callback,
@config.subject_name,
@provider.provides(@config.resource)
{@subject_name, @strategy.name, :register}
)
}
method="POST"
class={override_for(@socket, :form_class)}
>
<%= hidden_input(form, :action, value: "register") %>
<Input.identity_field socket={@socket} strategy={@strategy} form={form} />
<Input.password_field socket={@socket} strategy={@strategy} form={form} />
<Input.identity_field socket={@socket} config={@config} form={form} />
<Input.password_field socket={@socket} config={@config} form={form} />
<%= if @confirm? do %>
<Input.password_confirmation_field socket={@socket} config={@config} form={form} />
<%= if @strategy.confirmation_required? do %>
<Input.password_confirmation_field socket={@socket} strategy={@strategy} form={form} />
<% end %>
<%= if @inner_block do %>
@ -122,7 +114,7 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.RegisterFo
<Input.submit
socket={@socket}
config={@config}
strategy={@strategy}
form={form}
action={:register}
disable_text={override_for(@socket, :disable_button_text)}
@ -138,8 +130,7 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.RegisterFo
{:noreply, Socket.t()}
def handle_event("change", params, socket) do
config = socket.assigns.config
params = Map.get(params, to_string(config.subject_name))
params = get_params(params, socket.assigns.strategy)
form =
socket.assigns.form
@ -149,7 +140,7 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.RegisterFo
end
def handle_event("submit", params, socket) do
params = Map.get(params, to_string(socket.assigns.config.subject_name))
params = get_params(params, socket.assigns.strategy)
form = Form.validate(socket.assigns.form, params)
@ -160,4 +151,14 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.RegisterFo
{:noreply, socket}
end
defp get_params(params, strategy) do
param_key =
strategy.resource
|> Info.authentication_subject_name!()
|> to_string()
|> slugify()
Map.get(params, param_key, %{})
end
end

View file

@ -1,4 +1,4 @@
defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.ResetForm do
defmodule AshAuthentication.Phoenix.Components.Password.ResetForm do
use AshAuthentication.Phoenix.Overrides.Overridable,
root_class: "CSS class for the root `div` element.",
label_class: "CSS class for the `h2` element.",
@ -13,53 +13,45 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.ResetForm
## Component hierarchy
This is a child of `AshAuthentication.Phoenix.Components.PasswordAuthentication`.
This is a child of `AshAuthentication.Phoenix.Components.Password`.
Children:
* `AshAuthentication.Phoenix.Components.PasswordAuthentication.Input.identity_field/1`
* `AshAuthentication.Phoenix.Components.PasswordAuthentication.Input.submit/1`
* `AshAuthentication.Phoenix.Components.Password.Input.identity_field/1`
* `AshAuthentication.Phoenix.Components.Password.Input.submit/1`
## Props
* `config` - The configuration map as per
`AshAuthentication.authenticated_resources/1`.
Required.
* `label` - The text to show in the submit label.
Generated from the configured action name (via
`Phoenix.HTML.Form.humanize/1`) if not supplied.
Set to `false` to disable.
* `strategy` - The configuration map as per
`AshAuthentication.Info.strategy/2`. Required.
* `label` - The text to show in the submit label. Generated from the
configured action name (via `Phoenix.HTML.Form.humanize/1`) if not
supplied. Set to `false` to disable.
#{AshAuthentication.Phoenix.Overrides.Overridable.generate_docs()}
"""
use Phoenix.LiveComponent
alias AshAuthentication.{
PasswordAuthentication,
PasswordReset,
Phoenix.Components.PasswordAuthentication.Input
}
alias AshAuthentication.{Info, Phoenix.Components.Password.Input}
alias AshPhoenix.Form
alias Phoenix.LiveView.{Rendered, Socket}
import Phoenix.HTML.Form
import AshAuthentication.Phoenix.Components.Helpers
import Slug
@doc false
@impl true
@spec update(Socket.assigns(), Socket.t()) :: {:ok, Socket.t()}
def update(assigns, socket) do
config = assigns.config
form = blank_form(config)
strategy = assigns.strategy
form = blank_form(strategy)
socket =
socket
|> assign(assigns)
|> assign(form: form)
|> assign_new(:label, fn ->
humanize(PasswordReset.Info.request_password_reset_action_name!(config.resource))
end)
|> assign(form: form, subject_name: Info.authentication_subject_name!(strategy.resource))
|> assign_new(:label, fn -> strategy.request_password_reset_action_name end)
|> assign_new(:inner_block, fn -> nil end)
{:ok, socket}
@ -84,19 +76,15 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.ResetForm
phx-change="change"
phx-target={@myself}
action={
route_helpers(@socket).auth_request_path(
route_helpers(@socket).auth_path(
@socket.endpoint,
:request,
@config.subject_name,
@provider.provides(@config.resource)
{@subject_name, @strategy.name, :reset_request}
)
}
method="POST"
class={override_for(@socket, :form_class)}
>
<%= hidden_input(form, :action, value: "request_password_reset") %>
<Input.identity_field socket={@socket} config={@config} form={form} />
<Input.identity_field socket={@socket} strategy={@strategy} form={form} />
<%= if @inner_block do %>
<div class={override_for(@socket, :slot_class)}>
@ -106,7 +94,7 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.ResetForm
<Input.submit
socket={@socket}
config={@config}
strategy={@strategy}
form={form}
action={:request_reset}
disable_text={override_for(@socket, :disable_button_text)}
@ -122,8 +110,7 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.ResetForm
{:noreply, Socket.t()}
def handle_event("change", params, socket) do
config = socket.assigns.config
params = Map.get(params, to_string(config.subject_name))
params = get_params(params, socket.assigns.strategy)
form =
socket.assigns.form
@ -133,8 +120,8 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.ResetForm
end
def handle_event("submit", params, socket) do
config = socket.assigns.config
params = Map.get(params, to_string(config.subject_name))
strategy = socket.assigns.strategy
params = get_params(params, strategy)
socket.assigns.form
|> Form.validate(params)
@ -144,7 +131,7 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.ResetForm
socket =
socket
|> assign(:form, blank_form(config))
|> assign(:form, blank_form(strategy))
socket =
if flash do
@ -157,14 +144,28 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.ResetForm
{:noreply, socket}
end
defp blank_form(config) do
action = PasswordReset.Info.request_password_reset_action_name!(config.resource)
defp get_params(params, strategy) do
param_key =
strategy.resource
|> Info.authentication_subject_name!()
|> to_string()
|> slugify()
config.resource
|> Form.for_action(action,
api: config.api,
as: to_string(config.subject_name),
id: "#{PasswordAuthentication.provides(config.resource)}_#{config.subject_name}_#{action}"
Map.get(params, param_key, %{})
end
defp blank_form(%{resettable: [resettable]} = strategy) do
api = Info.authentication_api!(strategy.resource)
subject_name = Info.authentication_subject_name!(strategy.resource)
strategy.resource
|> Form.for_action(resettable.request_password_reset_action_name,
api: api,
as: subject_name |> to_string(),
id:
"#{subject_name}-#{strategy.name}-#{resettable.request_password_reset_action_name}"
|> slugify(),
context: %{strategy: strategy}
)
end
end

View file

@ -1,4 +1,4 @@
defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.SignInForm do
defmodule AshAuthentication.Phoenix.Components.Password.SignInForm do
use AshAuthentication.Phoenix.Overrides.Overridable,
root_class: "CSS class for the root `div` element.",
label_class: "CSS class for the `h2` element.",
@ -11,41 +11,39 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.SignInForm
## Component hierarchy
This is a child of `AshAuthentication.Phoenix.Components.PasswordAuthentication`.
This is a child of `AshAuthentication.Phoenix.Components.Password`.
Children:
* `AshAuthentication.Phoenix.Components.PasswordAuthentication.Input.identity_field/1`
* `AshAuthentication.Phoenix.Components.PasswordAuthentication.Input.password_field/1`
* `AshAuthentication.Phoenix.Components.PasswordAuthentication.Input.submit/1`
* `AshAuthentication.Phoenix.Components.Password.Input.identity_field/1`
* `AshAuthentication.Phoenix.Components.Password.Input.password_field/1`
* `AshAuthentication.Phoenix.Components.Password.Input.submit/1`
## Props
* `socket` - Phoenix LiveView socket. This is needed to be able to retrieve
the correct CSS configuration.
Required.
* `config` - The configuration map as per
`AshAuthentication.authenticated_resources/1`.
Required.
* `label` - The text to show in the submit label.
Generated from the configured action name (via
`Phoenix.HTML.Form.humanize/1`) if not supplied.
Set to `false` to disable.
the correct CSS configuration. Required.
* `strategy` - The configuration map as per
`AshAuthentication.Info.strategy/2`. Required.
* `label` - The text to show in the submit label. Generated from the
configured action name (via `Phoenix.HTML.Form.humanize/1`) if not
supplied. Set to `false` to disable.
#{AshAuthentication.Phoenix.Overrides.Overridable.generate_docs()}
"""
use Phoenix.LiveComponent
alias AshAuthentication.PasswordAuthentication.Info
alias AshAuthentication.Phoenix.Components.PasswordAuthentication
alias AshAuthentication.Info
alias AshAuthentication.Phoenix.Components.Password
alias AshPhoenix.Form
alias Phoenix.LiveView.{Rendered, Socket}
import AshAuthentication.Phoenix.Components.Helpers, only: [route_helpers: 1]
import Phoenix.HTML.Form
import Slug
@type props :: %{
required(:socket) => Socket.t(),
required(:config) => AshAuthentication.resource_config(),
required(:strategy) => AshAuthentication.Strategy.t(),
optional(:label) => String.t() | false
}
@ -53,23 +51,24 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.SignInForm
@impl true
@spec update(props, Socket.t()) :: {:ok, Socket.t()}
def update(assigns, socket) do
config = assigns.config
action = Info.password_authentication_sign_in_action_name!(config.resource)
strategy = assigns.strategy
api = Info.authentication_api!(strategy.resource)
subject_name = Info.authentication_subject_name!(strategy.resource)
form =
config.resource
|> Form.for_action(action,
api: config.api,
as: to_string(config.subject_name),
id:
"#{AshAuthentication.PasswordAuthentication.provides(config.resource)}_#{config.subject_name}_#{action}"
strategy.resource
|> Form.for_action(strategy.sign_in_action_name,
api: api,
as: subject_name |> to_string(),
id: "#{subject_name}-#{strategy.name}-#{strategy.sign_in_action_name}" |> slugify(),
context: %{strategy: strategy}
)
socket =
socket
|> assign(assigns)
|> assign(form: form, trigger_action: false)
|> assign_new(:label, fn -> humanize(action) end)
|> assign(form: form, trigger_action: false, subject_name: subject_name)
|> assign_new(:label, fn -> humanize(strategy.sign_in_action_name) end)
|> assign_new(:inner_block, fn -> nil end)
{:ok, socket}
@ -93,20 +92,16 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.SignInForm
phx-trigger-action={@trigger_action}
phx-target={@myself}
action={
route_helpers(@socket).auth_callback_path(
route_helpers(@socket).auth_path(
@socket.endpoint,
:callback,
@config.subject_name,
@provider.provides(@config.resource)
{@subject_name, @strategy.name, :sign_in}
)
}
method="POST"
class={override_for(@socket, :form_class)}
>
<%= hidden_input(form, :action, value: "sign_in") %>
<PasswordAuthentication.Input.identity_field socket={@socket} config={@config} form={form} />
<PasswordAuthentication.Input.password_field socket={@socket} config={@config} form={form} />
<Password.Input.identity_field socket={@socket} strategy={@strategy} form={form} />
<Password.Input.password_field socket={@socket} strategy={@strategy} form={form} />
<%= if @inner_block do %>
<div class={override_for(@socket, :slot_class)}>
@ -114,9 +109,9 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.SignInForm
</div>
<% end %>
<PasswordAuthentication.Input.submit
<Password.Input.submit
socket={@socket}
config={@config}
strategy={@strategy}
form={form}
action={:sign_in}
disable_text={override_for(@socket, :disable_button_text)}
@ -132,8 +127,7 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.SignInForm
{:noreply, Socket.t()}
def handle_event("change", params, socket) do
config = socket.assigns.config
params = Map.get(params, to_string(config.subject_name))
params = get_params(params, socket.assigns.strategy)
form =
socket.assigns.form
@ -143,7 +137,7 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.SignInForm
end
def handle_event("submit", params, socket) do
params = Map.get(params, to_string(socket.assigns.config.subject_name))
params = get_params(params, socket.assigns.strategy)
form = Form.validate(socket.assigns.form, params)
socket =
@ -153,4 +147,14 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.SignInForm
{:noreply, socket}
end
defp get_params(params, strategy) do
param_key =
strategy.resource
|> Info.authentication_subject_name!()
|> to_string()
|> slugify()
Map.get(params, param_key, %{})
end
end

View file

@ -1,17 +1,18 @@
defmodule AshAuthentication.Phoenix.Components.SignIn do
use AshAuthentication.Phoenix.Overrides.Overridable,
root_class: "CSS class for the root `div` element.",
provider_class: "CSS class for a `div` surrounding each provider component."
strategy_class: "CSS class for a `div` surrounding each strategy component.",
show_banner: "Whether or not to show the banner."
@moduledoc """
Renders sign in mark-up for an authenticated resource.
This means that it will render sign-in UI for all of the authentication
providers for a resource.
strategies for a resource.
For each provider configured on the resource a component name is inferred
(e.g. `AshAuthentication.PasswordAuthentication` becomes
`AshAuthentication.Phoenix.Components.PasswordAuthentication`) and is rendered
For each strategy configured on the resource a component name is inferred
(e.g. `AshAuthentication.Strategy.Password` becomes
`AshAuthentication.Phoenix.Components.Strategy.Passowrd`) and is rendered
into the output.
## Component hierarchy
@ -20,36 +21,38 @@ defmodule AshAuthentication.Phoenix.Components.SignIn do
Children:
* `AshAuthentication.Phoenix.Components.PasswordAuthentication`.
## Props
* `config` - The configuration man as per
`AshAuthentication.authenticated_resources/1`. Required.
* `AshAuthentication.Phoenix.Components.Strategy.Password`.
#{AshAuthentication.Phoenix.Overrides.Overridable.generate_docs()}
"""
use Phoenix.LiveComponent
alias AshAuthentication.Phoenix.Components
alias AshAuthentication.{Info, Phoenix.Components, Strategy}
alias Phoenix.LiveView.{Rendered, Socket}
import AshAuthentication.Phoenix.Components.Helpers
import Slug
@doc false
@impl true
@spec update(Socket.assigns(), Socket.t()) :: {:ok, Socket.t()}
def update(assigns, socket) do
resources =
strategies_by_resource =
socket
|> otp_app_from_socket()
|> AshAuthentication.authenticated_resources()
|> Enum.group_by(& &1.subject_name)
|> Enum.sort_by(&elem(&1, 0))
|> Enum.sort_by(&Info.authentication_subject_name!/1)
|> Enum.map(fn resource ->
resource
|> Info.authentication_strategies()
|> Enum.group_by(&strategy_style/1)
|> Map.update(:form, [], &sort_strategies_by_name/1)
|> Map.update(:link, [], &sort_strategies_by_name/1)
end)
socket =
socket
|> assign(assigns)
|> assign(:resources, resources)
|> assign(:strategies_by_resource, strategies_by_resource)
{:ok, socket}
end
@ -60,22 +63,32 @@ defmodule AshAuthentication.Phoenix.Components.SignIn do
def render(assigns) do
~H"""
<div class={override_for(@socket, :root_class)}>
<.live_component module={Components.Banner} socket={@socket} id="sign-in-banner" />
<%= if override_for(@socket, :show_banner, true) do %>
<.live_component module={Components.Banner} socket={@socket} id="sign-in-banner" />
<% end %>
<%= for {_subject_name, configs} <- @resources do %>
<%= for config <- configs do %>
<%= if has_form?(config) do %>
<%= for widget <- form_components(config) do %>
<.widget widget={widget} socket={@socket} />
<% end %>
<%= for strategies <- @strategies_by_resource do %>
<%= if Enum.any?(strategies.form) do %>
<%= for strategy <- strategies.form do %>
<.strategy
component={component_for_strategy(strategy)}
strategy={strategy}
socket={@socket}
/>
<% end %>
<%= if has_form?(config) && has_links?(config) do %>
<.live_component module={Components.HorizontalRule} id="hr" />
<% end %>
<%= if has_links?(config) do %>
<%= for widget <- link_components(config) do %>
<.widget widget={widget} socket={@socket} />
<% end %>
<% end %>
<%= if Enum.any?(strategies.form) && Enum.any?(strategies.link) do %>
<.live_component module={Components.HorizontalRule} id="sign-in-hr" />
<% end %>
<%= if Enum.any?(strategies.link) do %>
<%= for strategy <- strategies.link do %>
<.strategy
component={component_for_strategy(strategy)}
strategy={strategy}
socket={@socket}
/>
<% end %>
<% end %>
<% end %>
@ -83,50 +96,33 @@ defmodule AshAuthentication.Phoenix.Components.SignIn do
"""
end
defp widget(assigns) do
defp strategy(assigns) do
~H"""
<div class={override_for(@socket, :provider_class)}>
<.live_component
module={@widget.component}
id={provider_id(@widget.provider, @widget.config)}
provider={@widget.provider}
config={@widget.config}
/>
<div class={override_for(@socket, :strategy_class)}>
<.live_component module={@component} id={strategy_id(@strategy)} strategy={@strategy} />
</div>
"""
end
defp provider_id(provider, config) do
"sign-in-#{config.subject_name}-with-#{provider.provides(config.resource)}"
defp sort_strategies_by_name(strategies), do: Enum.sort_by(strategies, & &1.name)
defp strategy_id(strategy) do
subject_name =
strategy.resource
|> Info.authentication_subject_name!()
"sign-in-#{subject_name}-with-#{strategy.name}"
|> slugify()
end
defp has_form?(config), do: config |> form_components() |> Enum.any?()
defp has_links?(config), do: config |> link_components() |> Enum.any?()
defp strategy_style(%Strategy.Password{}), do: :form
defp strategy_style(_), do: :link
defp form_components(config) do
config
|> provider_components()
|> Enum.filter(& &1.component.form?())
end
defp link_components(config) do
config
|> provider_components()
|> Enum.filter(& &1.component.link?())
end
defp provider_components(%{providers: providers, resource: resource} = config) do
providers
|> Enum.sort_by(& &1.provides(resource))
|> Enum.map(fn provider ->
component =
provider
|> Module.split()
|> List.last()
|> then(&Module.concat(Components, &1))
%{component: component, provider: provider, config: config}
end)
|> Enum.filter(&Code.ensure_loaded?(&1.component))
defp component_for_strategy(strategy) do
strategy.__struct__
|> Module.split()
|> List.replace_at(1, "Components")
|> List.insert_at(1, "Phoenix")
|> Module.concat()
end
end

View file

@ -15,20 +15,20 @@ defmodule AshAuthentication.Phoenix.Controller do
use MyAppWeb, :controller
use AshAuthentication.Phoenix.Controller
def success(conn, user, _token) do
def success(conn, _activity, user, _token) do
conn
|> store_in_session(user)
|> assign(:current_user, user)
|> redirect(to: Routes.page_path(conn, :index))
end
def failure(conn, _reason) do
def failure(conn, _activity, _reason) do
conn
|> put_status(401)
|> render("failure.html")
end
def sign_out(conn) do
def sign_out(conn, _params) do
conn
|> clear_session()
|> render("sign_out.html")
@ -44,7 +44,7 @@ defmodule AshAuthentication.Phoenix.Controller do
use AshAuthentication.Phoenix.Controller
alias AshAuthentication.TokenRevocation
def success(conn, _user, token) do
def success(conn, _activity, _user, token) do
conn
|> put_status(200)
|> json(%{
@ -54,7 +54,7 @@ defmodule AshAuthentication.Phoenix.Controller do
})
end
def failure(conn, _reason) do
def failure(conn, _activity, _reason) do
conn
|> put_status(401)
|> json(%{
@ -64,7 +64,7 @@ defmodule AshAuthentication.Phoenix.Controller do
})
end
def sign_out(conn) do
def sign_out(conn, _params) do
conn
|> revoke_bearer_tokens()
|> json(%{
@ -76,72 +76,50 @@ defmodule AshAuthentication.Phoenix.Controller do
"""
@typedoc false
@type routes :: %{
required({String.t(), String.t()}) => %{
required(:provider) => module,
optional(atom) => any
}
}
alias AshAuthentication.Plug.Dispatcher
alias Plug.Conn
@doc false
@callback request(Conn.t(), %{required(String.t()) => String.t()}) :: Conn.t()
@doc false
@callback callback(Conn.t(), %{required(String.t()) => String.t()}) :: Conn.t()
@type t :: module
@type activity :: {strategy_name :: atom, phase :: atom}
@type user :: Ash.Resource.record() | nil
@type token :: String.t() | nil
@doc """
Called when authentication (or registration, depending on the provider) has been successful.
"""
@callback success(Conn.t(), user :: Ash.Resource.record(), token :: String.t()) :: Conn.t()
@callback success(Conn.t(), activity, user, token) :: Conn.t()
@doc """
Called when authentication fails.
"""
@callback failure(Conn.t(), nil | Ash.Changeset.t() | Ash.Error.t()) :: Conn.t()
@callback failure(Conn.t(), activity, reason :: any) :: Conn.t()
@doc """
Called when a request to sign out is received.
"""
@callback sign_out(Conn.t(), map) :: Conn.t()
@callback sign_out(Conn.t(), params :: map) :: Conn.t()
@doc false
@spec __using__(any) :: Macro.t()
defmacro __using__(_opts) do
quote do
@behaviour AshAuthentication.Phoenix.Controller
@behaviour AshAuthentication.Plug
import Phoenix.Controller
import Plug.Conn
import AshAuthentication.Phoenix.Plug
@doc false
@impl true
@spec request(Conn.t(), map) :: Conn.t()
def request(conn, params),
do:
AshAuthentication.Phoenix.Controller.request(
conn,
params,
__MODULE__
)
@doc false
@impl true
@spec callback(Conn.t(), map) :: Conn.t()
def callback(conn, params),
do:
AshAuthentication.Phoenix.Controller.callback(
conn,
params,
__MODULE__
)
@doc false
@impl true
@spec success(Conn.t(), Ash.Resource.record(), nil | AshAuthentication.Jwt.token()) ::
@spec success(
Conn.t(),
AshAuthentication.Phoenix.Controller.activity(),
AshAuthentication.Phoenix.Controller.user(),
AshAuthentication.Phoenix.Controller.token()
) ::
Conn.t()
def success(conn, user, _token) do
def success(conn, _activity, user, _token) do
conn
|> store_in_session(user)
|> put_status(200)
@ -150,8 +128,9 @@ defmodule AshAuthentication.Phoenix.Controller do
@doc false
@impl true
@spec failure(Conn.t(), nil | Ash.Changeset.t() | Ash.Error.t()) :: Conn.t()
def failure(conn, _) do
@spec failure(Conn.t(), AshAuthentication.Phoenix.Controller.activity(), reason :: any) ::
Conn.t()
def failure(conn, _activity, _reason) do
conn
|> put_status(401)
|> render("failure.html")
@ -166,95 +145,64 @@ defmodule AshAuthentication.Phoenix.Controller do
|> render("sign_out.html")
end
defoverridable success: 3, failure: 2, sign_out: 2
end
end
@doc false
@spec request(Conn.t(), %{required(String.t()) => String.t()}, module) :: Conn.t()
def request(conn, params, return_to) do
handle(conn, params, :request, return_to)
end
@doc false
@spec callback(Conn.t(), %{required(String.t()) => String.t()}, module) :: Conn.t()
def callback(conn, params, return_to) do
handle(conn, params, :callback, return_to)
end
defp handle(conn, _params, phase, return_to) do
conn
|> generate_routes()
|> dispatch(phase, conn)
|> return(return_to)
end
defp dispatch(
routes,
phase,
%{params: %{"subject_name" => subject_name, "provider" => provider}} = conn
) do
case Map.get(routes, {subject_name, provider}) do
config when is_map(config) ->
conn = Conn.put_private(conn, :authenticator, config)
case phase do
:request -> config.provider.request_plug(conn, [])
:callback -> config.provider.callback_plug(conn, [])
end
_ ->
@doc false
@impl true
@spec call(Conn.t(), any) :: Conn.t()
def call(%{private: %{strategy: strategy}} = conn, {_subject_name, _stategy_name, phase}) do
conn
|> Dispatcher.call({phase, strategy, __MODULE__})
end
def call(conn, opts) do
super(conn, opts)
end
@doc false
@impl true
@spec handle_success(
Conn.t(),
AshAuthentication.Phoenix.Controller.activity(),
AshAuthentication.Phoenix.Controller.user(),
AshAuthentication.Phoenix.Controller.token()
) :: Conn.t()
def handle_success(conn, activity, user, token) do
conn
|> put_private(:phoenix_action, :success)
|> put_private(:success_args, [activity, user, token])
|> call(:success)
end
@doc false
@impl true
@spec handle_failure(Conn.t(), AshAuthentication.Phoenix.Controller.activity(), any) ::
Conn.t()
def handle_failure(conn, activity, reason) do
conn
|> put_private(:phoenix_action, :failure)
|> put_private(:failure_args, [activity, reason])
|> call(:failure)
end
@doc false
@spec action(Conn.t(), any) :: Conn.t()
def action(conn, opts) do
conn
|> action_name()
|> case do
:success ->
args = Map.get(conn.private, :success_args, [nil, nil, nil])
apply(__MODULE__, :success, [conn | args])
:failure ->
args = Map.get(conn.private, :failure_args, [nil, nil])
apply(__MODULE__, :failure, [conn | args])
_ ->
super(conn, opts)
end
end
defoverridable success: 4, failure: 3, sign_out: 2
end
end
defp dispatch(_routes, _phase, conn), do: conn
defp return(
%{
private: %{
authentication_result: {:success, user},
authenticator: %{resource: resource}
}
} = conn,
return_to
)
when is_struct(user, resource),
do: return_to.success(conn, user, Map.get(user.__metadata__, :token))
defp return(%{private: %{authentication_result: {:success, nil}}} = conn, return_to),
do: return_to.success(conn, nil, nil)
defp return(%{private: %{authentication_result: {:failure, reason}}} = conn, return_to),
do: return_to.failure(conn, reason)
defp return(conn, return_to), do: return_to.failure(conn, nil)
# Doing this on every request is probably a really bad idea, but if I do it at
# compile time I need to ask for the OTP app all over the place and it reduces
# the developer experience sharply.
#
# Maybe we should just shove them in ETS?
defp generate_routes(conn) do
:otp_app
|> conn.private.phoenix_endpoint.config()
|> AshAuthentication.authenticated_resources()
|> Stream.flat_map(fn config ->
subject_name =
config.subject_name
|> to_string()
config
|> Map.get(:providers, [])
|> Stream.map(fn provider ->
config =
config
|> Map.delete(:providers)
|> Map.put(:provider, provider)
{{subject_name, provider.provides(config.resource)}, config}
end)
end)
|> Map.new()
end
end

View file

@ -9,25 +9,30 @@ defmodule AshAuthentication.Phoenix.Overrides do
You can override by setting the following in your `config.exs`:
```elixir
config :my_app, AshAuthentication.Phoenix, overrides: [MyAppWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.Default]
config :my_app, AshAuthentication.Phoenix,
overrides: [MyAppWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.Default]
```
and defining `lib/my_app_web/auth_styles.ex` within which you can set CSS
classes for any values you want.
and defining `lib/my_app_web/auth_overrides.ex` within which you can set any
overrides.
The `use` macro defines overridable versions of all callbacks which return
`nil`, so you only need to define the functions that you care about.
Each of the override modules specified in the config will be called in the
order that they're specified, so you can still use the defaults if you just
override some properties.
```elixir
defmodule MyAppWeb.AuthOverrides do
use AshAuthentication.Phoenix.Overrides
alias AshAuthentication.Phoenix.Components
override
override Components.Banner do
set :image_url, "/images/sign_in_logo.png"
end
end
```
## Configuration
"""
@doc """
@ -45,6 +50,8 @@ defmodule AshAuthentication.Phoenix.Overrides do
end)
end
@doc false
@spec __using__(any) :: Macro.t()
defmacro __using__(_env) do
quote do
require AshAuthentication.Phoenix.Overrides
@ -55,6 +62,10 @@ defmodule AshAuthentication.Phoenix.Overrides do
end
end
@doc """
Define overrides for a specific component.
"""
@spec override(component :: module, do: Macro.t()) :: Macro.t()
defmacro override(component, do: block) do
quote do
@component unquote(component)
@ -62,12 +73,18 @@ defmodule AshAuthentication.Phoenix.Overrides do
end
end
@doc """
Override a setting within a component.
"""
@spec set(atom, any) :: Macro.t()
defmacro set(selector, value) do
quote do
@override {@component, unquote(selector), unquote(value)}
end
end
@doc false
@spec __before_compile__(any) :: Macro.t()
defmacro __before_compile__(env) do
overrides =
env.module

View file

@ -40,7 +40,7 @@ defmodule AshAuthentication.Phoenix.Overrides.Default do
set :text, "or"
end
override Components.PasswordAuthentication do
override Components.Password do
set :root_class, "mt-4 mb-4"
set :interstitial_class, "flex flex-row justify-between content-between text-sm font-medium"
set :toggler_class, "flex-none text-blue-500 hover:text-blue-600"
@ -51,7 +51,7 @@ defmodule AshAuthentication.Phoenix.Overrides.Default do
set :hide_class, "hidden"
end
override Components.PasswordAuthentication.SignInForm do
override Components.Password.SignInForm do
set :root_class, nil
set :label_class, "mt-2 mb-4 text-2xl tracking-tight font-bold text-gray-900"
set :form_class, nil
@ -59,7 +59,7 @@ defmodule AshAuthentication.Phoenix.Overrides.Default do
set :disable_button_text, "Signing in ..."
end
override Components.PasswordAuthentication.RegisterForm do
override Components.Password.RegisterForm do
set :root_class, nil
set :label_class, "mt-2 mb-4 text-2xl tracking-tight font-bold text-gray-900"
set :form_class, nil
@ -67,7 +67,7 @@ defmodule AshAuthentication.Phoenix.Overrides.Default do
set :disable_button_text, "Registering ..."
end
override Components.PasswordAuthentication.ResetForm do
override Components.Password.ResetForm do
set :root_class, nil
set :label_class, "mt-2 mb-4 text-2xl tracking-tight font-bold text-gray-900"
set :form_class, nil
@ -79,7 +79,7 @@ defmodule AshAuthentication.Phoenix.Overrides.Default do
set :disable_button_text, "Requesting ..."
end
override Components.PasswordAuthentication.Input do
override Components.Password.Input do
set :field_class, "mt-2 mb-2"
set :label_class, "block text-sm font-medium text-gray-700 mb-1"
@ -106,7 +106,7 @@ defmodule AshAuthentication.Phoenix.Overrides.Default do
set :input_debounce, 350
end
override Components.OAuth2Authentication do
override Components.OAuth2 do
set :root_class, "w-full mt-2 mb-4"
set :link_class, """

View file

@ -42,7 +42,9 @@ defmodule AshAuthentication.Phoenix.Overrides.Overridable do
end
end
@doc false
@doc """
Retrieve configuration for a potentially overriden value.
"""
@spec override_for(Socket.t(), atom, any) :: any
defmacro override_for(socket, selector, default \\ nil) do
overrides =

View file

@ -13,7 +13,7 @@ defmodule AshAuthentication.Phoenix.Plug do
Attempt to retrieve all actors from the connections' session.
A wrapper around `AshAuthentication.Plug.Helpers.retrieve_from_session/2`
with the `otp_app` already present.
with the `otp_app` as extracted from the endpoint.
"""
@spec load_from_session(Conn.t(), any) :: Conn.t()
def load_from_session(conn, _opts) do
@ -25,7 +25,8 @@ defmodule AshAuthentication.Phoenix.Plug do
@doc """
Attempt to retrieve actors from the `Authorization` header(s).
A wrapper around `AshAuthentication.Plug.Helpers.retrieve_from_bearer/2` with the `otp_app` already present.
A wrapper around `AshAuthentication.Plug.Helpers.retrieve_from_bearer/2` with
the `otp_app` as extracted from the endpoint.
"""
@spec load_from_bearer(Conn.t(), any) :: Conn.t()
def load_from_bearer(conn, _opts) do
@ -36,7 +37,8 @@ defmodule AshAuthentication.Phoenix.Plug do
@doc """
Revoke all token(s) in the `Authorization` header(s).
A wrapper around `AshAuthentication.Plug.Helpers.revoke_bearer_tokens/2` with the `otp_app` already present.
A wrapper around `AshAuthentication.Plug.Helpers.revoke_bearer_tokens/2` with
the `otp_app` as extracted from the endpoint.
"""
@spec revoke_bearer_tokens(Conn.t(), any) :: Conn.t()
def revoke_bearer_tokens(conn, _opts) do

View file

@ -28,16 +28,28 @@ defmodule AshAuthentication.Phoenix.Router do
pipe_through :browser
sign_in_route
sign_out_route AuthController
auth_routes AuthController
auth_routes_for MyApp.Accounts.User, to: AuthController
end
```
"""
require Logger
@typedoc "Options that can be passed to `auth_routes_for`."
@type auth_route_options :: [path_option | to_option | scope_opts_option]
@typedoc "A sub-path if required. Defaults to `/auth`."
@type path_option :: {:path, String.t()}
@typedoc "The controller which will handle success and failure."
@type to_option :: {:to, AshAuthentication.Phoenix.Controller.t()}
@typedoc "Any options which should be passed to the generated scope."
@type scope_opts_option :: {:scope_opts, keyword}
@doc false
@spec __using__(any) :: Macro.t()
defmacro __using__(_opts) do
defmacro __using__(_) do
quote do
import AshAuthentication.Phoenix.Router
import AshAuthentication.Phoenix.Plug
@ -45,23 +57,51 @@ defmodule AshAuthentication.Phoenix.Router do
end
@doc """
Generates the routes needed for the various subjects and providers
authenticating with AshAuthentication.
Generates the routes needed for the various strategies for a given
AshAuthentication resource.
This is required if you wish to use authentication.
## Options
* `to` - a module which implements the
`AshAuthentication.Phoenix.Controller` behaviour. This is required.
* `path` - a string (starting with "/") wherein to mount the generated
routes.
* `scope_opts` - any options to pass to the generated scope.
## Example
```elixir
scope "/", DevWeb do
auth_routes_for(MyApp.Accounts.User,
to: AuthController,
path: "/authentication",
scope_opts: [host: "auth.example.com"]
)
end
```
"""
defmacro auth_routes(auth_controller, path \\ "auth", opts \\ []) do
opts =
opts
|> Keyword.put_new(:as, :auth)
@spec auth_routes_for(Ash.Resource.t(), auth_route_options) :: Macro.t()
defmacro auth_routes_for(resource, opts) when is_list(opts) do
quote location: :keep do
subject_name = AshAuthentication.Info.authentication_subject_name!(unquote(resource))
controller = Keyword.fetch!(unquote(opts), :to)
path = Keyword.get(unquote(opts), :path, "/auth")
scope_opts = Keyword.get(unquote(opts), :scope_opts, [])
quote do
scope unquote(path), unquote(opts) do
match(:*, "/:subject_name/:provider", unquote(auth_controller), :request, as: :request)
strategies =
AshAuthentication.Info.authentication_add_ons(unquote(resource)) ++
AshAuthentication.Info.authentication_strategies(unquote(resource))
match(:*, "/:subject_name/:provider/callback", unquote(auth_controller), :callback,
as: :callback
)
scope path, scope_opts do
for strategy <- strategies do
for {path, phase} <- AshAuthentication.Strategy.routes(strategy) do
match :*, path, controller, {subject_name, strategy.name, phase},
as: :auth,
private: %{strategy: strategy}
end
end
end
end
end

View file

@ -1,6 +1,7 @@
defmodule AshAuthentication.Phoenix.SignInLive do
use AshAuthentication.Phoenix.Overrides.Overridable,
root_class: "CSS class for the root `div` element."
root_class: "CSS class for the root `div` element.",
sign_in_id: "Element ID for the `SignIn` LiveComponent."
@moduledoc """
A generic, white-label sign-in page.
@ -27,7 +28,7 @@ defmodule AshAuthentication.Phoenix.SignInLive do
def render(assigns) do
~H"""
<div class={override_for(@socket, :root_class)}>
<.live_component module={Components.SignIn} id="sign-in" />
<.live_component module={Components.SignIn} id={override_for(@socket, :sign_in_id, "sign-in")} />
</div>
"""
end

View file

@ -87,7 +87,7 @@ defmodule AshAuthentication.Phoenix.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:ash_authentication, github: "team-alembic/ash_authentication", tag: "v1.0.0"},
{:ash_authentication, github: "team-alembic/ash_authentication", tag: "v3.0.3"},
{:ash_phoenix, "~> 1.1"},
{:ash, "~> 2.2"},
{:jason, "~> 1.0"},
@ -95,6 +95,7 @@ defmodule AshAuthentication.Phoenix.MixProject do
{:phoenix_live_view, "~> 0.18"},
{:phoenix, "~> 1.6"},
{:bcrypt_elixir, "~> 3.0"},
{:slugify, "~> 1.3"},
{:credo, "~> 1.6", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 1.2", only: [:dev, :test], runtime: false},
{:doctor, "~> 0.18", only: [:dev, :test]},

View file

@ -1,6 +1,6 @@
%{
"ash": {:hex, :ash, "2.4.11", "a9fd2616b4ade692361140a0501b03533e60ad9a7a67615041b697daf802efd5", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: true]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:spark, "~> 0.2 and >= 0.2.10", [hex: :spark, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.5.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0da0b4a8da4247cc14e2ba139c6b8175e33416fa962faba80725bdbf12da355e"},
"ash_authentication": {:git, "https://github.com/team-alembic/ash_authentication.git", "5471e498d0114e85e5940dab20aadd77050731ee", [tag: "v1.0.0"]},
"ash": {:hex, :ash, "2.4.2", "ba579e6654c32b1da49f17938d2f1445066f27e61eedbf0fae431b816b49d1be", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: true]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:spark, ">= 0.2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.5.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "da8f94a19cf29617526ca2b1a75f6fae804c1db7c825b49982c603f503a615bd"},
"ash_authentication": {:git, "https://github.com/team-alembic/ash_authentication.git", "20c7c20b108c6f5d547b4db0d8f0890e7b59f5e7", [tag: "v3.0.3"]},
"ash_phoenix": {:hex, :ash_phoenix, "1.1.2", "36c46852fa0e739d7217092ac71e3a72e729d5c00758f401234215d8d32f350b", [:mix], [{:ash, "~> 2.0", [hex: :ash, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5.6 or ~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.15", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "9afb2c9e8dbb68d3549713b4a96b56801c250489efdf4776b931770bd629e0bd"},
"assent": {:hex, :assent, "0.2.1", "46ad0ed92b72330f38c60bc03c528e8408475dc386f48d4ecd18833cfa581b9f", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "58c558b6029ffa287e15b38c8e07cd99f0b24e4846c52abad0c0a6225c4873bc"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.0.1", "9be815469e6bfefec40fa74658ecbbe6897acfb57614df1416eeccd4903f602c", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "486bb95efb645d1efc6794c1ddd776a186a9a713abf06f45708a6ce324fb96cf"},
@ -17,7 +17,7 @@
"docsh": {:hex, :docsh, "0.7.2", "f893d5317a0e14269dd7fe79cf95fb6b9ba23513da0480ec6e77c73221cae4f2", [:rebar3], [{:providers, "1.8.1", [hex: :providers, repo: "hexpm", optional: false]}], "hexpm", "4e7db461bb07540d2bc3d366b8513f0197712d0495bb85744f367d3815076134"},
"doctor": {:hex, :doctor, "0.20.0", "2a8ff8f87eaf3fc78f20ffcfa7a3181f2bdb6a115a4abd52582e6156a89649a5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "36ba43bdf7d799c41e1dc00b3429eb48bc5d4dc3f63b181ca1aa8829ec638862"},
"earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"},
"ecto": {:hex, :ecto, "3.9.2", "017db3bc786ff64271108522c01a5d3f6ba0aea5c84912cfb0dd73bf13684108", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "21466d5177e09e55289ac7eade579a642578242c7a3a9f91ad5c6583337a9d15"},
"ecto": {:hex, :ecto, "3.9.1", "67173b1687afeb68ce805ee7420b4261649d5e2deed8fe5550df23bab0bc4396", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c80bb3d736648df790f7f92f81b36c922d9dd3203ca65be4ff01d067f54eb304"},
"elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"},
"elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "9e16517a05e48eb7b39d3db190a00a136cb05f8d", []},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
@ -52,8 +52,9 @@
"plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
"providers": {:hex, :providers, "1.8.1", "70b4197869514344a8a60e2b2a4ef41ca03def43cfb1712ecf076a0f3c62f083", [:rebar3], [{:getopt, "1.0.1", [hex: :getopt, repo: "hexpm", optional: false]}], "hexpm", "e45745ade9c476a9a469ea0840e418ab19360dc44f01a233304e118a44486ba0"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"},
"sourceror": {:hex, :sourceror, "0.11.2", "549ce48be666421ac60cfb7f59c8752e0d393baa0b14d06271d3f6a8c1b027ab", [:mix], [], "hexpm", "9ab659118896a36be6eec68ff7b0674cba372fc8e210b1e9dc8cf2b55bb70dfb"},
"spark": {:hex, :spark, "0.2.12", "03ebab9ed1ecc577c65fd1ae8b88c41d5ba8420b393658616a657d6d0fc2996f", [:mix], [{:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "22dfba98a9a6ebb5a21d520fa79cf3e67f9f549fff1c6ade55aa6c1d26814463"},
"spark": {:hex, :spark, "0.2.17", "90c201fefe02eba9611733454c6b330ec2adc6eb823ce7aa1f6c83e38c05cd62", [:mix], [{:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "a2a9375bb6d06aab960c6990688258d820160e454e3d22e9669ea0eda37a2e07"},
"stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"},
"telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
"typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"},

View file

@ -4,5 +4,6 @@ defmodule Example.Accounts.Registry do
entries do
entry Example.Accounts.User
entry Example.Accounts.Token
end
end

View file

@ -0,0 +1,10 @@
defmodule Example.Accounts.Token do
@moduledoc false
use Ash.Resource,
data_layer: Ash.DataLayer.Ets,
extensions: [AshAuthentication.TokenResource]
token do
api Example.Accounts
end
end

View file

@ -1,10 +0,0 @@
defmodule Example.Accounts.TokenRevocation do
@moduledoc false
use Ash.Resource,
data_layer: Ash.DataLayer.Ets,
extensions: [AshAuthentication.TokenRevocation]
revocation do
api Example.Accounts
end
end

View file

@ -2,13 +2,7 @@ defmodule Example.Accounts.User do
@moduledoc false
use Ash.Resource,
data_layer: Ash.DataLayer.Ets,
extensions: [
AshAuthentication,
AshAuthentication.Confirmation,
AshAuthentication.PasswordAuthentication,
AshAuthentication.PasswordReset,
AshAuthentication.OAuth2Authentication
]
extensions: [AshAuthentication]
require Logger
@ -50,53 +44,60 @@ defmodule Example.Accounts.User do
update_timestamp(:updated_at)
end
# wat 2
authentication do
api(Example.Accounts)
end
confirmation do
monitor_fields([:email])
add_ons do
confirmation :confirm do
monitor_fields([:email])
sender(fn user, token ->
Logger.debug("Confirmation request for #{user.email} with token #{inspect(token)}")
end)
end
sender(fn user, token ->
Logger.debug("Confirmation request for #{user.email} with token #{inspect(token)}")
end)
end
end
password_authentication do
identity_field(:email)
hashed_password_field(:hashed_password)
end
strategies do
password :password do
identity_field(:email)
hashed_password_field(:hashed_password)
password_reset do
sender(fn user, token ->
Logger.debug("Password reset request for #{user.email} with token #{inspect(token)}")
end)
end
resettable do
sender(fn user, token ->
Logger.debug("Password reset request for #{user.email} with token #{inspect(token)}")
end)
end
end
oauth2_authentication do
provider_name(:auth0)
client_id(&get_config/3)
redirect_uri(&get_config/3)
client_secret(&get_config/3)
site(&get_config/3)
oauth2 :auth0 do
client_id(&get_config/2)
redirect_uri(&get_config/2)
client_secret(&get_config/2)
site(&get_config/2)
authorize_path("/authorize")
token_path("/oauth/token")
user_path("/userinfo")
authorization_params(scope: "openid profile email")
auth_method(:client_secret_post)
end
authorize_path("/authorize")
token_path("/oauth/token")
user_path("/userinfo")
authorization_params(scope: "openid profile email")
auth_method(:client_secret_post)
end
end
tokens do
enabled?(true)
revocation_resource(Example.Accounts.TokenRevocation)
tokens do
enabled?(true)
token_resource(Example.Accounts.Token)
end
end
identities do
identity(:unique_email, [:email], pre_check_with: Example.Accounts)
identity(:unique_email, [:email],
pre_check_with: Example.Accounts,
eager_check_with: Example.Accounts
)
end
def get_config(path, resource, _opts) do
def get_config(path, resource) do
value =
:ash_authentication_phoenix
|> Application.get_env(resource, [])