feat(PasswordReset): Add a generic password reset form (#37)

* feat(PasswordReset): Add a generic password reset form

* improvement(Input.submit): trim trailing "with password" from submit buttons.
This commit is contained in:
James Harton 2022-12-15 16:31:52 +13:00 committed by GitHub
parent 90b0a45363
commit 432f056905
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 423 additions and 12 deletions

View file

@ -21,7 +21,7 @@ config :ash_authentication_phoenix, DevWeb.Endpoint,
config :ash_authentication_phoenix, ash_apis: [Example.Accounts], namespace: Dev
config :ash_authentication, AshAuthentication.Jwt,
config :ash_authentication_phoenix,
signing_secret: "All I wanna do is to thank you, even though I don't know who you are."
config :phoenix, :json_library, Jason

View file

@ -34,5 +34,6 @@ defmodule DevWeb.Router do
auth_routes_for(Example.Accounts.User, to: AuthController, path: "/auth")
sign_in_route(overrides: [DevWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.Default])
sign_out_route(AuthController, "/sign-out")
reset_route()
end
end

View file

@ -230,13 +230,19 @@ defmodule AshAuthentication.Phoenix.Components.Password.Input do
:request_reset ->
assigns.strategy.resettable
|> Enum.map(& &1.request_password_reset_action_name)
|> List.first() || :reqest_reset
|> List.first(:request_reset)
|> to_string()
|> String.trim_trailing("_with_password")
:sign_in ->
assigns.strategy.sign_in_action_name
|> to_string()
|> String.trim_trailing("_with_password")
:register ->
assigns.strategy.register_action_name
|> to_string()
|> String.trim_trailing("_with_password")
end
|> humanize()
end)

View file

@ -141,7 +141,7 @@ defmodule AshAuthentication.Phoenix.Components.Password.ResetForm do
|> Form.validate(params)
|> Form.submit()
flash = override_for(socket, :reset_flash_text)
flash = override_for(socket.assigns.overrides, :reset_flash_text)
socket =
socket

View file

@ -0,0 +1,84 @@
defmodule AshAuthentication.Phoenix.Components.Reset do
use AshAuthentication.Phoenix.Overrides.Overridable,
root_class: "CSS class for the root `div` element.",
strategy_class: "CSS class for a `div` surrounding each strategy component.",
show_banner: "Whether or not to show the banner."
@moduledoc """
Renders a password-reset form.
## Component heirarchy
Children:
* `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
* `token` - The reset token.
* `overrides` - A list of override modules.
#{AshAuthentication.Phoenix.Overrides.Overridable.generate_docs()}
"""
use Phoenix.LiveComponent
alias AshAuthentication.{Info, Phoenix.Components, Strategy.Password}
alias Phoenix.LiveView.{Rendered, Socket}
import AshAuthentication.Phoenix.Components.Helpers
@type props :: %{
required(:token) => String.t(),
optional(:overrides) => [module]
}
@doc false
@impl true
@spec update(props, Socket.t()) :: {:ok, Socket.t()}
def update(assigns, socket) do
strategies =
socket
|> otp_app_from_socket()
|> AshAuthentication.authenticated_resources()
|> Enum.sort_by(&Info.authentication_subject_name!/1)
|> Stream.flat_map(&Info.authentication_strategies/1)
|> Stream.filter(&is_struct(&1, Password))
|> Enum.filter(&Enum.any?(&1.resettable))
socket =
socket
|> assign(assigns)
|> assign(strategies: strategies)
|> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end)
{:ok, socket}
end
@doc false
@impl true
@spec render(Socket.assigns()) :: Rendered.t() | no_return
def render(assigns) do
~H"""
<div class={override_for(@overrides, :root_class)}>
<%= if override_for(@overrides, :show_banner, true) do %>
<.live_component module={Components.Banner} socket={@socket} id="sign-in-banner" />
<% end %>
<%= for strategy <- @strategies do %>
<div class={override_for(@overrides, :strategy_class)}>
<.live_component
module={Components.Reset.Form}
strategy={strategy}
token={@token}
socket={@socket}
id="reset-form"
label={false}
overrides={@overrides}
/>
</div>
<% end %>
</div>
"""
end
end

View file

@ -0,0 +1,183 @@
defmodule AshAuthentication.Phoenix.Components.Reset.Form do
use AshAuthentication.Phoenix.Overrides.Overridable,
root_class: "CSS class for the root `div` element.",
label_class: "CSS class for the `h2` element.",
form_class: "CSS class for the `form` element.",
spacer_class: "CSS classes for space between the password input and submit elements.",
disable_button_text: "Text for the submit button when the request is happening."
@moduledoc """
Generates a default password reset form.
## Component heirarchy
This is a child of `AshAuthentication.Phoenix.Components.Reset`.
Children:
* `AshAuthentication.Phoenix.Components.Password.Input.identity_field/1`
* `AshAuthentication.Phoenix.Components.Password.Input.password_field/1`
* `AshAuthentication.Phoenix.Components.Password.Input.submit/1`
* `AshAuthentication.Phoenix.Components.Password.Input.error/1`
## Props
* `token` - The reset token.
* `socket` - Phoenix LiveView socket. This is needed to be able to retrieve
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.{Info, Phoenix.Components.Password.Input}
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(:strategy) => AshAuthentication.Strategy.t(),
required(:token) => String.t(),
optional(:label) => String.t() | false,
optional(:overrices) => [module]
}
@doc false
@impl true
@spec update(props, Socket.t()) :: {:ok, Socket.t()}
def update(assigns, socket) do
strategy = assigns.strategy
api = Info.authentication_api!(strategy.resource)
subject_name = Info.authentication_subject_name!(strategy.resource)
[resettable] = strategy.resettable
form =
strategy.resource
|> Form.for_action(resettable.password_reset_action_name,
api: api,
as: subject_name |> to_string(),
id:
"#{subject_name}-#{strategy.name}-#{resettable.password_reset_action_name}" |> slugify(),
context: %{strategy: strategy}
)
socket =
socket
|> assign(assigns)
|> assign(
form: form,
trigger_action: false,
subject_name: subject_name,
resettable: resettable
)
|> assign_new(:label, fn -> humanize(resettable.password_reset_action_name) end)
|> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end)
{:ok, socket}
end
@doc false
@impl true
@spec render(Socket.assigns()) :: Rendered.t() | no_return
def render(assigns) do
~H"""
<div class={override_for(@overrides, :root_class)}>
<%= if @label do %>
<h2 class={override_for(@overrides, :label_class)}><%= @label %></h2>
<% end %>
<.form
:let={form}
for={@form}
phx-change="change"
phx-submit="submit"
phx-trigger-action={@trigger_action}
phx-target={@myself}
action={
route_helpers(@socket).auth_path(@socket.endpoint, {@subject_name, @strategy.name, :reset})
}
method="POST"
class={override_for(@overrides, :form_class)}
>
<%= hidden_input(form, :reset_token, value: @token) %>
<Input.error socket={@socket} field={:reset_token} form={@form} overrides={@overrides} />
<Input.password_field
socket={@socket}
strategy={@strategy}
form={form}
overrides={@overrides}
/>
<%= if @strategy.confirmation_required? do %>
<Input.password_confirmation_field
socket={@socket}
strategy={@strategy}
form={form}
overrides={@overrides}
/>
<% end %>
<div class={override_for(@overrides, :spacer_class)}></div>
<Input.submit
socket={@socket}
strategy={@strategy}
form={form}
action={:reset}
disable_text={override_for(@overrides, :disable_button_text)}
label={humanize(@resettable.password_reset_action_name)}
overrides={@overrides}
/>
</.form>
</div>
"""
end
@doc false
@impl true
@spec handle_event(String.t(), %{required(String.t()) => String.t()}, Socket.t()) ::
{:noreply, Socket.t()}
def handle_event("change", params, socket) do
params = get_params(params, socket.assigns.strategy)
form =
socket.assigns.form
|> Form.validate(params, errors: false)
{:noreply, assign(socket, form: form)}
end
def handle_event("submit", params, socket) do
params = get_params(params, socket.assigns.strategy)
form = Form.validate(socket.assigns.form, params)
socket =
socket
|> assign(:form, form)
|> assign(:trigger_action, form.valid?)
{: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

@ -7,7 +7,7 @@ defmodule AshAuthentication.Phoenix.Overrides do
generic looking user interface.
You can override this by adding your own override modules to the
`AshAuthentication.Phoenix.Router.sign_in_route/3` macro in your router:
`AshAuthentication.Phoenix.Router.sign_in_route/1` macro in your router:
```elixir
sign_in_route overrides: [MyAppWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.Default]

View file

@ -6,19 +6,40 @@ defmodule AshAuthentication.Phoenix.Overrides.Default do
"""
use AshAuthentication.Phoenix.Overrides
alias AshAuthentication.Phoenix.{Components, SignInLive}
alias AshAuthentication.Phoenix.{Components, ResetLive, SignInLive}
override SignInLive do
set :root_class, "grid h-screen place-items-center"
end
override ResetLive do
set :root_class, "grid h-screen place-items-center"
end
override Components.Reset do
set :root_class, """
flex-1 flex flex-col justify-center py-12 px-4 sm:px-6 lg:flex-none
lg:px-20 xl:px-24
"""
set :strategy_class, "mx-auth w-full max-w-sm lg:w-96"
end
override Components.Reset.Form 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
set :spacer_class, "py-1"
set :disable_button_text, "Changing password ..."
end
override Components.SignIn do
set :root_class, """
flex-1 flex flex-col justify-center py-12 px-4 sm:px-6 lg:flex-none
lg:px-20 xl:px-24
"""
set :provider_class, "mx-auth w-full max-w-sm lg:w-96"
set :strategy_class, "mx-auth w-full max-w-sm lg:w-96"
end
override Components.Banner do
@ -98,7 +119,7 @@ defmodule AshAuthentication.Phoenix.Overrides.Default do
w-full flex justify-center py-2 px-4 border border-transparent rounded-md
shadow-sm text-sm font-medium text-white bg-blue-500 hover:bg-blue-600
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500
mt-2 mb-4
mt-4 mb-4
"""
set :error_ul, "text-red-400 font-light my-3 italic text-sm"

View file

@ -0,0 +1,59 @@
defmodule AshAuthentication.Phoenix.ResetLive do
use AshAuthentication.Phoenix.Overrides.Overridable,
root_class: "CSS class for the root `div` element.",
reset_id: "Element ID for the `Reset` LiveComponent."
@moduledoc """
A generic, white-label password reset page.
This live-view can be rendered into your app using the
`AshAuthentication.Phoenix.Router.reset_route/1` macro in your router (or by
using `Phoenix.LiveView.Controller.live_render/3` directly in your markup).
This live-view looks for the `token` URL parameter, and if found passes it to
`AshAuthentication.Phoenix.Components.Reset`.
#{AshAuthentication.Phoenix.Overrides.Overridable.generate_docs()}
"""
use Phoenix.LiveView
alias AshAuthentication.Phoenix.Components
alias Phoenix.LiveView.{Rendered, Socket}
@doc false
@impl true
def mount(_params, session, socket) do
overrides =
session
|> Map.get("overrides", [AshAuthentication.Phoenix.Overrides.Default])
socket =
socket
|> assign(overrides: overrides)
{:ok, socket}
end
@doc false
@impl true
@spec handle_params(map, String.t(), Socket.t()) :: {:noreply, Socket.t()}
def handle_params(%{"token" => token}, _uri, socket) do
{:noreply, assign(socket, :token, token)}
end
@doc false
@impl true
@spec render(Socket.assigns()) :: Rendered.t()
def render(assigns) do
~H"""
<div class={override_for(@overrides, :root_class)}>
<.live_component
module={Components.Reset}
id={override_for(@overrides, :reset_id, "reset")}
token={@token}
overrides={@overrides}
/>
</div>
"""
end
end

View file

@ -29,6 +29,7 @@ defmodule AshAuthentication.Phoenix.Router do
sign_in_route
sign_out_route AuthController
auth_routes_for MyApp.Accounts.User, to: AuthController
reset_route
end
```
"""
@ -174,4 +175,56 @@ defmodule AshAuthentication.Phoenix.Router do
end
end
end
@doc """
Generates a generic, white-label password reset page using LiveView and the
components in `AshAuthentication.Phoenix.Components`.
Available options are:
* `path` the path under which to mount the live-view. Defaults to
`"/password-reset"`.
* `live_view` the name of the live view to render. Defaults to
`AshAuthentication.Phoenix.ResetLive`.
* `as` which is passed to the generated `live` route. Defaults to `:auth`.
* `overrides` specify any override modules for customisation. See
`AshAuthentication.Phoenix.Overrides` for more information. all other
options are passed to the generated `scope`.
This is completely optional.
"""
@spec reset_route(
opts :: [
{:path, String.t()}
| {:live_view, module}
| {:as, atom}
| {:overrides, [module]}
| {atom, any}
]
) :: Macro.t()
defmacro reset_route(opts \\ []) do
{path, opts} = Keyword.pop(opts, :path, "/password-reset")
{live_view, opts} = Keyword.pop(opts, :live_view, AshAuthentication.Phoenix.ResetLive)
{as, opts} = Keyword.pop(opts, :as, :auth)
{overrides, opts} =
Keyword.pop(opts, :overrides, [AshAuthentication.Phoenix.Overrides.Default])
opts =
opts
|> Keyword.put_new(:alias, false)
quote do
scope unquote(path), unquote(opts) do
import Phoenix.LiveView.Router, only: [live: 4, live_session: 2]
live_session :reset do
live("/:token", unquote(live_view), :reset,
as: unquote(as),
private: %{overrides: unquote(overrides)}
)
end
end
end
end
end

View file

@ -6,8 +6,8 @@ defmodule AshAuthentication.Phoenix.SignInLive do
@moduledoc """
A generic, white-label sign-in page.
This live-view can be rendered into your app by using the
`AshAuthentication.Phoenix.Router.sign_in_route/3` macro in your router (or by
This live-view can be rendered into your app using the
`AshAuthentication.Phoenix.Router.sign_in_route/1` macro in your router (or by
using `Phoenix.LiveView.Controller.live_render/3` directly in your markup).
This live-view finds all Ash resources with an authentication configuration

View file

@ -89,7 +89,7 @@ defmodule AshAuthentication.Phoenix.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:ash_authentication, "~> 3.0"},
{:ash_authentication, "~> 3.1"},
{:ash_phoenix, "~> 1.1"},
{:ash, "~> 2.2"},
{:jason, "~> 1.0"},

View file

@ -1,6 +1,6 @@
%{
"ash": {:hex, :ash, "2.4.24", "fb74aaf9ee8d9c8397c1c57d2d2ebf48cc0b3a933736eab66b25cea72826ac9f", [: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]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:spark, "~> 0.2.18", [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", "e1b7ac0cf41af75f954bfb9500d1aeb54e4b3b34ec41dbe71c685b92ef8304ae"},
"ash_authentication": {:hex, :ash_authentication, "3.0.3", "a00a19a2c60a115d530e27ab5a2d768f998069e61d66073254be477394e42361", [:mix], [{:ash, "~> 2.4", [hex: :ash, repo: "hexpm", optional: false]}, {:assent, "~> 0.2", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:mint, "~> 1.4", [hex: :mint, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 0.2.12", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "689e0b3735ab95c6e30c484ae34bf6edb5a66fb37befa4cb7dab77ebd93ddaa8"},
"ash_authentication": {:hex, :ash_authentication, "3.1.0", "b6f30a2f1107de113b56e1a29889e385acbccc62cea37dcf2f3a453477396a7a", [:mix], [{:ash, "~> 2.4", [hex: :ash, repo: "hexpm", optional: false]}, {:assent, "~> 0.2", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:mint, "~> 1.4", [hex: :mint, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 0.2.12", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "4cbf27735037cec0d63745a3081d285f71ff873c990871afcea64049b3dc06ff"},
"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"},
@ -18,7 +18,7 @@
"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"},
"elixir_make": {:hex, :elixir_make, "0.7.1", "314f2a5450254db0446ba94cc1ba12a25b83b457f24aa9cc21c128cead5d03aa", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "0f1ad4787b4d7489563351cbf85c9221a852f5441364a2cb3ffd36f2fda7f7fb"},
"elixir_make": {:hex, :elixir_make, "0.7.2", "e83548b0500e654d1a595f1134af4862a2e92ec3282ec4c2a17641e9aa45ee73", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "05fb44abf9582381c2eb1b73d485a55288c581071de0ee3ee1084ee69d6a8e5f"},
"elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "9e16517a05e48eb7b39d3db190a00a136cb05f8d", []},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ets": {:hex, :ets, "0.8.1", "8ff9bcda5682b98493f8878fc9dbd990e48d566cba8cce59f7c2a78130da29ea", [:mix], [], "hexpm", "6be41b50adb5bc5c43626f25ea2d0af1f4a242fb3fad8d53f0c67c20b78915cc"},

View file

@ -87,6 +87,10 @@ defmodule Example.Accounts.User do
tokens do
enabled?(true)
token_resource(Example.Accounts.Token)
signing_secret(fn _, _ ->
Application.fetch_env(:ash_authentication_phoenix, :signing_secret)
end)
end
end