feat: Add support for PasswordAuthentication.

This commit is contained in:
James Harton 2022-10-25 17:02:56 +13:00
parent c3bbac966b
commit 05ab4f438b
23 changed files with 1533 additions and 47 deletions

View file

@ -1,5 +1,5 @@
%Doctor.Config{
ignore_modules: [~r/^Inspect\./, ~r/.Plug$/],
ignore_modules: [~r/^Inspect\./, ~r/.Plug$/, AshAuthentication.Phoenix.Overrides],
ignore_paths: [],
min_module_doc_coverage: 40,
min_module_spec_coverage: 0,

View file

@ -1,5 +1,19 @@
# Used by "mix format"
[
import_deps: [:ash, :ash_authentication, :phoenix],
inputs: ["{mix,.formatter}.exs", "{dev,config,lib,test}/**/*.{ex,exs}"]
inputs: ["{mix,.formatter}.exs", "{dev,config,lib,test}/**/*.{ex,exs}"],
export: [
locals_without_parens: [
sign_in_route: 0,
sign_in_route: 1,
sign_in_route: 2,
sign_in_route: 3,
sign_out_route: 1,
sign_out_route: 2,
sign_out_route: 3,
auth_routes: 1,
auth_routes: 2,
auth_routes: 3
]
]
]

View file

@ -1,11 +1,14 @@
# AshAuthenticationPhoenix
# AshAuthentication.Phoenix
**TODO: Add description**
The `ash_authentication_phoenix` package extends
[`ash_authentication`](https://github.com/team-alembic/ash_authentication) by
adding router helpers, plugs and behaviours that makes adding authentication to
an existing Ash-based Phoenix application dead easy.
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `ash_authentication_phoenix` to your list of dependencies in `mix.exs`:
The package can be installed by adding `ash_authentication_phoenix` to your list
of dependencies in `mix.exs`:
```elixir
def deps do
@ -15,7 +18,59 @@ def deps do
end
```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/ash_authentication_phoenix>.
## Usage
This package assumes that you have [Phoenix](https://phoenixframework.org/),
[Ash](https://ash-hq.org/) and
[AshAuthentication](https://github.com/team-alembic/ash_authentication)
installed and configured. See their individual documentation for details.
This package is designed so that you can choose the level of customisation
required. At the easiest level of configuration, you can just add the routes
into your router:
```elixir
defmodule MyAppWeb.Router do
use MyAppWeb, :router
use AshAuthentication.Phoenix.Router
pipeline :browser do
# ...
plug(:load_from_session)
end
scope "/" do
pipe_through :browser
sign_in_route
sign_out_route MyAppWeb.AuthController
auth_routes MyAppWeb.AuthController
end
end
```
This will give you a generic sign-in/registration page and store the
authenticated user in the Phoenix session.
### Customisation
There are several methods of customisation available depending on the level of
control you would like:
1. Use the [generic sign-in liveview](https://hexdocs.pm/ash_authentication_phoenix/AshAuthentication.Phoenix.SignInLive.html).
2. Apply [overrides](https://hexdocs.pm/ash_authentication_phoenix/AshAuthentication.Phoenix.Overrides.html)
to set your own CSS classes for all components.
3. Build your own sign-in pages using the pre-defined components.
4. Build your own sign-in pages using the generated `auth` routes.
## Documentation
Documentation for the latest release will be [available on
hexdocs](https://hexdocs.pm/ash_authentication_phoenix) and for the [`main`
branch](https://team-alembic.github.io/ash_authentication_phoenix).
## Contributing
* To contribute updates, fixes or new features please fork and open a pull-request against `main`.
* Please use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) - this allows us to dynamically generate the changelog.
* Feel free to ask any questions on out [GitHub discussions page](https://github.com/team-alembic/ash_authentication_phoenix/discussions).

View file

@ -19,7 +19,7 @@ config :ash_authentication_phoenix, DevWeb.Endpoint,
secret_key_base: "5PmCh9zQTJuCjlXm2EeF+hoYLkFxgH/3bzLE8D0Tzg5XLw6ZIMGipHFbr0z19dlC",
server: true
config :ash_authentication_phoenix, ash_apis: [Example.Accounts]
config :ash_authentication_phoenix, ash_apis: [Example.Accounts], namespace: Dev
config :ash_authentication, AshAuthentication.JsonWebToken,
signing_secret: "All I wanna do is to thank you, even though I don't know who you are."

View file

@ -2,14 +2,13 @@ defmodule DevWeb.AuthController do
@moduledoc false
use DevWeb, :controller
# use AshPhoenix.Authentication.Controller
alias Plug.Conn
use AshAuthentication.Phoenix.Controller
@doc false
@impl true
def success(conn, user, _token) do
conn
# |> store_in_session(user)
|> store_in_session(user)
|> assign(:current_user, user)
|> put_status(200)
|> render("success.html")

View file

@ -2,9 +2,10 @@ defmodule DevWeb.PageController do
@moduledoc false
use DevWeb, :controller
alias Plug.Conn
@doc false
@impl true
@spec index(Conn.t(), %{required(String.t()) => String.t()}) :: Conn.t()
def index(conn, _params) do
render(conn, "index.html")
end

View file

@ -2,7 +2,7 @@ defmodule DevWeb.Router do
@moduledoc false
use DevWeb, :router
# use AshPhoenix.Authentication.Router
use AshAuthentication.Phoenix.Router
pipeline :browser do
plug :accepts, ["html"]
@ -11,7 +11,7 @@ defmodule DevWeb.Router do
plug :put_root_layout, {DevWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
# plug :load_from_session
plug :load_from_session
end
pipeline :api do
@ -29,10 +29,10 @@ defmodule DevWeb.Router do
# pipe_through :api
# end
# scope "/" do
# pipe_through :browser
# auth_routes DevWeb.AuthController, "/auth"
# sign_in_route "/sign-in"
# sign_out_route DevWeb.AuthController, "/sign-out"
# end
scope "/", DevWeb do
pipe_through :browser
auth_routes(AuthController, "/auth")
sign_in_route("/sign-in")
sign_out_route(AuthController, "/sign-out")
end
end

View file

@ -1,18 +1,32 @@
defmodule AshAuthenticationPhoenix do
defmodule AshAuthentication.Phoenix do
@moduledoc """
Documentation for `AshAuthenticationPhoenix`.
"""
Welcome to `AshAuthentication.Pheonix`.
@doc """
Hello world.
The `ash_authentication_phoenix` package extends
[`ash_authentication`](https://github.com/team-alembic/ash_authentication) by
adding router helpers, plugs and behaviours that makes adding authentication
to an existing Ash-based Phoenix application dead easy.
## Examples
## Where to start.
iex> AshAuthenticationPhoenix.hello()
:world
Presuming that you already have [Phoenix](https://phoenixframework.org/),
[Ash](https://ash-hq.org/) and
[AshAuthentication](https://github.com/team-alembic/ash_authentication)
installed and configured, start by adding plugs and routes to your router
using `AshAuthentication.Phoenix.Router` and customising your sign-in page as
needed.
### Customisation
There are several methods of customisation available depending on the level of
control you would like:
1. Use the generic sign-in liveview -
`AshAuthentication.Phoenix.SignInLive`.
2. Apply overrides using `AshAuthentication.Phoenix.Overrides` to set your
own CSS classes for all components.
3. Build your own sign-in pages using the pre-defined components.
4. Build your own sign-in pages using the generated `auth` routes.
"""
def hello do
:world
end
end

View file

@ -0,0 +1,43 @@
defmodule AshAuthentication.Phoenix.Components.Helpers do
@moduledoc """
Helpers which are commonly needed inside the various components.
"""
alias Phoenix.LiveView.Socket
@doc """
The LiveView `Socket` contains a reference to the Phoenix endpoint, and from
there we can extract the `otp_app` of the current request.
"""
@spec otp_app_from_socket(Socket.t()) :: atom
def otp_app_from_socket(socket) do
:otp_app
|> socket.endpoint.config()
end
@doc """
The LiveView `Socket` contains a refererence to the Phoenix router, and from
there we can generate the name of the route helpers module.
"""
@spec route_helpers(Socket.t()) :: module
def route_helpers(socket) do
Module.concat(socket.router, Helpers)
end
@doc """
Find the override for a specific element.
Uses `otp_app_from_socket/1` to find the OTP application name and then extract
the configuration from the application environment.
"""
@spec override_for(Socket.t(), atom) :: nil | String.t()
def override_for(socket, name) do
module =
socket
|> otp_app_from_socket()
|> Application.get_env(AshAuthentication.Phoenix, [])
|> Keyword.get(:override_module, AshAuthentication.Phoenix.Overrides.Default)
apply(module, name, [])
end
end

View file

@ -0,0 +1,78 @@
defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication do
@default_debounce 750
@moduledoc """
Generates sign in and registration forms for a resource.
## Component heirarchy
This is the top-most provider-specific component, nested below `AshAuthentication.Phoenix.Components.SignIn`.
Children:
* `AshAuthentication.Phoenix.Components.PasswordAuthentication.SignInForm`
* `AshAuthentication.Phoenix.Components.PasswordAuthentication.RegisterForm`
## Props
* `config` - The configuration man as per
`AshAuthentication.authenticated_resources/1`.
Required.
* `debounce` - The number of milliseconds to wait before firing a change
event to prevent too many events being fired to the server.
Defaults to `#{@default_debounce}`.
* `show_forms` - Explicitly enable/disable a specific form.
A list containing `:sign_in`, `:register` or both.
* `spacer` - A string containing text to display in the spacer element.
Defaults to `"or"`.
Set to `false` to disable.
Also disabled if `show_forms` does not contain both forms.
## Overrides
See `AshAuthentication.Phoenix.Overrides` for more information.
* `password_authentication_box_css_class` - applied to the root `div` element of this component.
* `password_authentication_box_spacer_css_class` - applied to the spacer element, if enabled.
"""
use Phoenix.LiveComponent
alias __MODULE__
alias AshAuthentication.PasswordAuthentication.Info
alias Phoenix.LiveView.Rendered
import AshAuthentication.Phoenix.Components.Helpers
@type props :: %{
required(:config) => AshAuthentication.resource_config(),
optional(:debounce) => millis :: pos_integer(),
optional(:spacer) => String.t() | false,
optional(:show_forms) => [:sign_in | :register]
}
@doc false
@spec render(props) :: Rendered.t() | no_return
def render(assigns) do
assigns =
assigns
|> assign(:sign_in_action, Info.sign_in_action_name!(assigns.config.resource))
|> assign(:register_action, Info.register_action_name!(assigns.config.resource))
|> assign_new(:debounce, fn -> @default_debounce end)
|> assign_new(:spacer, fn -> "or" end)
|> assign_new(:show_forms, fn -> [:sign_in, :register] end)
~H"""
<div class={override_for(@socket, :password_authentication_box_css_class)}>
<%= if :sign_in in @show_forms do %>
<.live_component module={PasswordAuthentication.SignInForm} id={"#{@config.subject_name}_#{@provider.provides}_#{@sign_in_action}"} provider={@provider} config={@config} debounce={@debounce} />
<% end %>
<%= if length(@show_forms) > 1 && @spacer do %>
<div class={override_for(@socket, :password_authentication_box_spacer_css_class)}>
<%= @spacer %>
</div>
<% end %>
<%= if :register in @show_forms do %>
<.live_component module={PasswordAuthentication.RegisterForm} id={"#{@config.subject_name}_#{@provider.provides}_#{@register_action}"} provider={@provider} config={@config} debounce={@debounce} />
<% end %>
</div>
"""
end
end

View file

@ -0,0 +1,267 @@
defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
@moduledoc """
Function components for dealing with form input during password authentication.
## Component heirarchy
These function components are consumed by
`AshAuthentication.Phoenix.Components.PasswordAuthentication.SignInForm` and
`AshAuthentication.Phoenix.Components.PasswordAuthentication.RegisterForm`.
"""
use Phoenix.Component
alias AshAuthentication.PasswordAuthentication.Info
alias AshPhoenix.Form
alias Phoenix.LiveView.{Rendered, Socket}
import Phoenix.HTML.Form
import AshAuthentication.Phoenix.Components.Helpers
@doc """
Generate a form field for the configured identity field.
## 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.
* `form` - An `AshPhoenix.Form`.
Required.
* `input_type` - Either `:text` or `:email`.
If not set it will try and guess based on the name of the identity field.
## Overrides
See `AshAuthentication.Phoenix.Overrides` for more information.
* `password_authentication_form_input_surround_css_class` - applied to the div surrounding the `label` and
`input` elements.
* `password_authentication_form_label_css_class` - applied to the `label` tag.
* `password_authentication_form_text_input_css_class` - applied to the `input` tag.
"""
@spec identity_field(%{
required(:socket) => Socket.t(),
required(:config) => AshAuthentication.resource_config(),
required(:form) => AshPhoenix.Form.t(),
optional(:input_type) => :text | :email
}) :: Rendered.t() | no_return
def identity_field(assigns) do
identity_field = Info.identity_field!(assigns.config.resource)
assigns =
assigns
|> assign(:identity_field, identity_field)
|> assign_new(:input_type, fn ->
identity_field
|> to_string()
|> String.starts_with?("email")
|> then(fn
true -> :email
_ -> :text
end)
end)
~H"""
<div class={override_for(@socket, :password_authentication_form_input_surround_css_class)}>
<%= label @form, @identity_field, class: override_for(@socket, :password_authentication_form_label_css_class) %>
<%= text_input @form, @identity_field, type: to_string(@input_type), class: override_for(@socket, :password_authentication_form_text_input_css_class) %>
<.error socket={@socket} form={@form} field={@identity_field} />
</div>
"""
end
@doc """
Generate a form field for the configured password entry field.
## 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.
* `form` - An `AshPhoenix.Form`.
Required.
## Overrides
See `AshAuthentication.Phoenix.Overrides` for more information.
* `password_authentication_form_input_surround_css_class` - applied to the div surrounding the `label` and
`input` elements.
* `password_authentication_form_label_css_class` - applied to the `label` tag.
* `password_authentication_form_text_input_css_class` - applied to the `input` tag.
"""
@spec password_field(%{
required(:socket) => Socket.t(),
required(:config) => AshAuthentication.resource_config(),
required(:form) => AshPhoenix.Form.t()
}) :: Rendered.t() | no_return
def password_field(assigns) do
assigns =
assigns
|> assign(:password_field, Info.password_field!(assigns.config.resource))
~H"""
<div class={override_for(@socket, :password_authentication_form_input_surround_css_class)}>
<%= label @form, @password_field, class: override_for(@socket, :password_authentication_form_label_css_class) %>
<%= password_input @form, @password_field, class: override_for(@socket, :password_authentication_form_text_input_css_class), value: input_value(@form, @password_field) %>
<.error socket={@socket} form={@form} field={@password_field} />
</div>
"""
end
@doc """
Generate a form field for the configured password confirmation entry field.
## 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.
* `form` - An `AshPhoenix.Form`.
Required.
## Overrides
See `AshAuthentication.Phoenix.Overrides` for more information.
* `password_authentication_form_input_surround_css_class` - applied to the div surrounding the `label` and
`input` elements.
* `password_authentication_form_label_css_class` - applied to the `label` tag.
* `password_authentication_form_text_input_css_class` - applied to the `input` tag.
"""
@spec password_confirmation_field(%{
required(:socket) => Socket.t(),
required(:config) => AshAuthentication.resource_config(),
required(:form) => AshPhoenix.Form.t()
}) :: Rendered.t() | no_return
def password_confirmation_field(assigns) do
assigns =
assigns
|> assign(
:password_confirmation_field,
Info.password_confirmation_field!(assigns.config.resource)
)
~H"""
<div class={override_for(@socket, :password_authentication_form_input_surround_css_class)}>
<%= label @form, @password_confirmation_field, class: override_for(@socket, :password_authentication_form_label_css_class) %>
<%= password_input @form, @password_confirmation_field, class: override_for(@socket, :password_authentication_form_text_input_css_class), value: input_value(@form, @password_confirmation_field) %>
<.error socket={@socket} form={@form} field={@password_confirmation_field} />
</div>
"""
end
@doc """
Generate an form submit button.
## 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.
* `form` - An `AshPhoenix.Form`.
Required.
* `action` - Either `:sign_in` or `:register`.
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.
## Overrides
See `AshAuthentication.Phoenix.Overrides` for more information.
* `password_authentication_form_submit_css_class` - applied to the `button` element.
"""
@spec submit(%{
required(:socket) => Socket.t(),
required(:config) => AshAuthentication.resource_config(),
required(:form) => AshPhoenix.Form.t(),
required(:action) => :sign_in | :register,
optional(:label) => String.t()
}) :: Rendered.t() | no_return
def submit(assigns) do
assigns =
assigns
|> assign_new(:label, fn ->
case assigns.action do
:sign_in ->
assigns.config.resource
|> Info.sign_in_action_name!()
:register ->
assigns.config.resource
|> Info.register_action_name!()
end
|> humanize()
end)
~H"""
<%= submit @label, class: override_for(@socket, :password_authentication_form_submit_css_class) %>
"""
end
@doc """
Generate a list of errors for a field (if there are any).
## Props
* `socket` - Phoenix LiveView socket.
This is needed to be able to retrieve the correct CSS configuration.
Required.
* `form` - An `AshPhoenix.Form`.
Required.
* `field` - The field for which to retrieve the errors.
Required.
## Overrides
See `AshAuthentication.Phoenix.Overrides` for more information.
* `password_authentication_form_error_ul_css_class` - applied to the `ul` element.
* `password_authentication_form_error_li_css_class` - applied to the `li` element.
"""
@spec error(%{
required(:socket) => Socket.t(),
required(:form) => Form.t(),
required(:field) => atom,
optional(:field_label) => String.Chars.t(),
optional(:errors) => [{atom, String.t()}]
}) :: Rendered.t() | no_return
def error(assigns) do
assigns =
assigns
|> assign_new(:errors, fn ->
assigns.form
|> Form.errors()
|> Keyword.get_values(assigns.field)
end)
|> assign_new(:field_label, fn -> humanize(assigns.field) end)
~H"""
<%= if Enum.any?(@errors) do %>
<ul class={override_for(@socket, :password_authentication_form_error_ul_css_class)}>
<%= for error <- @errors do %>
<li class={override_for(@socket, :password_authentication_form_error_li_css_class)} phx-feedback-for={input_name(@form, @field)}>
<%= @field_label %> <%= error %>
</li>
<% end %>
</ul>
<% end %>
"""
end
end

View file

@ -0,0 +1,148 @@
defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.RegisterForm do
@default_debounce 750
@moduledoc """
Generates a default registration form.
## Component heirarchy
This is a child of `AshAuthentication.Phoenix.Components.PasswordAuthentication`.
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`
## 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.
* `debounce` - The number of milliseconds to wait before firing a change
event to prevent too many events being fired to the server.
Defaults to `#{@default_debounce}`.
## Overrides
See `AshAuthentication.Phoenix.Overrides` for more information.
* `password_authentication_form_h2_css_class` - applied to the `h2` element used to render the label.
* `password_authentication_form_css_class` - applied to the `form` element.
"""
use Phoenix.LiveComponent
alias AshAuthentication.PasswordAuthentication.Info
alias AshAuthentication.Phoenix.Components.PasswordAuthentication
alias AshPhoenix.Form
alias Phoenix.LiveView.{Rendered, Socket}
import Phoenix.HTML.Form
import AshAuthentication.Phoenix.Components.Helpers
@type props :: %{
required(:socket) => Socket.t(),
required(:config) => AshAuthentication.resource_config(),
optional(:label) => String.t() | false,
optional(:debounce) => millis :: pos_integer()
}
@doc false
@impl true
@spec update(props, Socket.t()) :: {:ok, Socket.t()}
def update(assigns, socket) do
config = assigns.config
action = Info.register_action_name!(config.resource)
confirm? = Info.confirmation_required?(config.resource)
form =
config.resource
|> Form.for_action(action,
api: config.api,
as: to_string(config.subject_name),
id:
"#{AshAuthentication.PasswordAuthentication.provides()}_#{config.subject_name}_#{action}"
)
socket =
socket
|> assign(assigns)
|> assign(form: form, trigger_action: false, confirm?: confirm?)
|> assign_new(:label, fn -> humanize(action) end)
|> assign_new(:debounce, fn -> @default_debounce end)
{:ok, socket}
end
@doc false
@impl true
@spec render(props) :: Rendered.t() | no_return
def render(assigns) do
~H"""
<div>
<%= if @label do %>
<h2 class={override_for(@socket, :password_authentication_form_h2_css_class)}><%= @label %></h2>
<% end %>
<.form
:let={f}
for={@form}
phx-change="change"
phx-submit="submit"
phx-trigger-action={@trigger_action}
phx-target={@myself}
phx-debounce={@debounce}
action={route_helpers(@socket).auth_callback_path(@socket.endpoint, :callback, @config.subject_name, @provider.provides)}
method="POST"
class={override_for(@socket, :password_authentication_form_css_class)}>
<%= hidden_input f, :action, value: "register" %>
<PasswordAuthentication.Input.identity_field socket={@socket} config={@config} form={f} />
<PasswordAuthentication.Input.password_field socket={@socket} config={@config} form={f} />
<%= if @confirm? do %>
<PasswordAuthentication.Input.password_confirmation_field socket={@socket} config={@config} form={f} />
<% end %>
<PasswordAuthentication.Input.submit socket={@socket} config={@config} form={f} action={:register}/>
</.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 = Map.get(params, to_string(socket.assigns.config.subject_name))
form =
socket.assigns.form
|> Form.validate(params)
socket =
socket
|> assign(:form, form)
{:noreply, socket}
end
def handle_event("submit", params, socket) do
params = Map.get(params, to_string(socket.assigns.config.subject_name))
form = Form.validate(socket.assigns.form, params)
socket =
socket
|> assign(:form, form)
|> assign(:trigger_action, form.valid?)
{:noreply, socket}
end
end

View file

@ -0,0 +1,137 @@
defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.SignInForm do
@default_debounce 750
@moduledoc """
Generates a default sign in form.
## Component heirarchy
This is a child of `AshAuthentication.Phoenix.Components.PasswordAuthentication`.
Children:
* `AshAuthentication.Phoenix.Components.PasswordAuthentication.Input.identity_field/1`
* `AshAuthentication.Phoenix.Components.PasswordAuthentication.Input.password_field/1`
* `AshAuthentication.Phoenix.Components.PasswordAuthentication.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.
* `debounce` - The number of milliseconds to wait before firing a change
event to prevent too many events being fired to the server.
Defaults to `#{@default_debounce}`.
## Overrides
See `AshAuthentication.Phoenix.Overrides` for more information.
* `password_authentication_form_h2_css_class` - applied to the `h2` element used to render the label.
* `password_authentication_form_css_class` - applied to the `form` element.
"""
use Phoenix.LiveComponent
alias AshAuthentication.PasswordAuthentication.Info
alias AshAuthentication.Phoenix.Components.PasswordAuthentication
alias AshPhoenix.Form
alias Phoenix.LiveView.{Rendered, Socket}
import Phoenix.HTML.Form
import AshAuthentication.Phoenix.Components.Helpers
@type props :: %{
required(:socket) => Socket.t(),
required(:config) => AshAuthentication.resource_config(),
optional(:label) => String.t() | false,
optional(:debounce) => millis :: pos_integer()
}
@doc false
@impl true
@spec update(props, Socket.t()) :: {:ok, Socket.t()}
def update(assigns, socket) do
config = assigns.config
action = Info.sign_in_action_name!(config.resource)
form =
config.resource
|> Form.for_action(action,
api: config.api,
as: to_string(config.subject_name),
id:
"#{AshAuthentication.PasswordAuthentication.provides()}_#{config.subject_name}_#{action}"
)
socket =
socket
|> assign(assigns)
|> assign(form: form, trigger_action: false)
|> assign_new(:label, fn -> humanize(action) end)
|> assign_new(:debounce, fn -> @default_debounce end)
{:ok, socket}
end
@doc false
@impl true
@spec render(props) :: Rendered.t() | no_return
def render(assigns) do
~H"""
<div>
<h2 class={override_for(@socket, :password_authentication_form_h2_css_class)}><%= @label %></h2>
<.form :let={f}
for={@form}
phx-change="change"
phx-submit="submit"
phx-trigger-action={@trigger_action}
phx-target={@myself}
phx-debounce={@debounce}
action={route_helpers(@socket).auth_callback_path(@socket.endpoint, :callback, @config.subject_name, @provider.provides())}
method="POST"
class={override_for(@socket, :password_authentication_form_css_class)}>
<%= hidden_input f, :action, value: "sign_in" %>
<PasswordAuthentication.Input.identity_field socket={@socket} config={@config} form={f} />
<PasswordAuthentication.Input.password_field socket={@socket} config={@config} form={f} />
<PasswordAuthentication.Input.submit socket={@socket} config={@config} form={f} action={:sign_in}/>
</.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 = Map.get(params, to_string(socket.assigns.config.subject_name))
form = Form.validate(socket.assigns.form, params)
socket =
socket
|> assign(:form, form)
{:noreply, socket}
end
def handle_event("submit", params, socket) do
params = Map.get(params, to_string(socket.assigns.config.subject_name))
form = Form.validate(socket.assigns.form, params)
socket =
socket
|> assign(:form, form)
|> assign(:trigger_action, form.valid?)
{:noreply, socket}
end
end

View file

@ -0,0 +1,65 @@
defmodule AshAuthentication.Phoenix.Components.SignIn do
@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.
For each provider configured on the resource a component name is inferred
(e.g. `AshAuthentication.PasswordAuthentication` becomes
`AshAuthentication.Phoenix.Components.PasswordAuthentication`) and is rendered
into the output.
## Component heirarchy
This is the top-most authentication component.
Children:
* `AshAuthentication.Phoenix.Components.PasswordAuthentication`.
## Props
* `config` - The configuration man as per
`AshAuthentication.authenticated_resources/1`. Required.
## Overrides
See `AshAuthentication.Phoenix.Overrides` for more information.
* `sign_in_box_css_class` - applied to the root `div` element of this component.
* `sign_in_row_css_class` - applied to the spacer element, if enabled.
"""
use Phoenix.LiveComponent
alias AshAuthentication.Phoenix.Components
alias Phoenix.LiveView.Rendered
import AshAuthentication.Phoenix.Components.Helpers
@type props :: %{required(:config) => AshAuthentication.resource_config()}
@doc false
@spec render(props) :: Rendered.t() | no_return
def render(assigns) do
~H"""
<div class={override_for(@socket, :sign_in_box_css_class)}>
<%= for provider <- @config.providers do %>
<div class={override_for(@socket, :sign_in_row_css_class)}>
<.live_component module={component_for_provider(provider)} id={provider_id(provider, @config)} provider={provider} config={@config} />
</div>
<% end %>
</div>
"""
end
defp component_for_provider(provider),
do:
provider
|> Module.split()
|> List.last()
|> then(&Module.concat(Components, &1))
defp provider_id(provider, config) do
"sign-in-#{config.subject_name}-#{provider.provides()}"
end
end

View file

@ -0,0 +1,251 @@
defmodule AshAuthentication.Phoenix.Controller do
@moduledoc """
The authentication controller generator.
Since authentication often requires explicit HTTP requests to do things like
set cookies or return Authorization headers, use this module to create an
`AuthController` in your Phoenix application.
## Example
Handling the registration or authentication of a normal web-based user.
```elixir
defmodule MyAppWeb.AuthController do
use MyAppWeb, :controller
use AshAuthentication.Phoenix.Controller
def success(conn, user, _token) do
conn
|> store_in_session(user)
|> assign(:current_user, user)
|> redirect(to: Routes.page_path(conn, :index))
end
def failure(conn) do
conn
|> put_status(401)
|> render("failure.html")
end
def sign_out(conn) do
conn
|> clear_session()
|> render("sign_out.html")
end
end
```
Handling registration or authentication of an API user.
```elixir
defmodule MyAppWeb.ApiAuthController do
use MyAppWeb, :controller
use AshAuthentication.Phoenix.Controller
alias AshAuthentication.TokenRevocation
def success(conn, _user, token) do
conn
|> put_status(200)
|> json(%{
authentication: %{
status: :success,
bearer: token}
})
end
def failure(conn) do
conn
|> put_status(401)
|> json(%{
authentication: %{
status: :failed
}
})
end
def sign_out(conn) do
conn
|> revoke_bearer_tokens()
|> json(%{
status: :ok
})
end
end
```
"""
@typedoc false
@type routes :: %{
required({String.t(), String.t()}) => %{
required(:provider) => module,
optional(atom) => any
}
}
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()
@doc """
Called when authentication (or registration, depending on the provider) has been successful.
"""
@callback success(Conn.t(), actor :: Ash.Resource.record(), token :: String.t()) :: Conn.t()
@doc """
Called when authentication fails.
"""
@callback failure(Conn.t(), nil | Ash.Changeset.t() | Ash.Error.t()) :: Conn.t()
@doc """
Called when a request to sign out is received.
"""
@callback sign_out(Conn.t(), map) :: Conn.t()
@doc false
@spec __using__(any) :: Macro.t()
defmacro __using__(_opts) do
quote do
@behaviour AshAuthentication.Phoenix.Controller
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()) ::
Conn.t()
def success(conn, actor, _token) do
conn
|> store_in_session(actor)
|> put_status(200)
|> render("success.html")
end
@doc false
@impl true
@spec failure(Conn.t(), nil | Ash.Changeset.t() | Ash.Error.t()) :: Conn.t()
def failure(conn, _) do
conn
|> put_status(401)
|> render("failure.html")
end
@doc false
@impl true
@spec sign_out(Conn.t(), map) :: Conn.t()
def sign_out(conn, _params) do
conn
|> clear_session()
|> 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,
%{"subject_name" => subject_name, "provider" => provider} = _params,
phase,
return_to
) do
routes = generate_routes(conn)
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
_ ->
conn
end
|> case do
%{state: :sent} ->
conn
%{private: %{authentication_result: {:success, actor}}} ->
return_to.success(conn, actor, Map.get(actor.__metadata__, :token))
%{private: %{authentication_result: {:failure, reason}}} ->
return_to.failure(conn, reason)
_ ->
return_to.failure(conn, nil)
end
end
defp handle(conn, _, _, return_to) do
return_to.failure(conn, nil)
end
# 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}
end)
end)
|> Map.new()
end
end

View file

@ -0,0 +1,99 @@
defmodule AshAuthentication.Phoenix.Overrides do
@configurables [
password_authentication_form_label_css_class: "CSS classes for generated `label` tags",
password_authentication_form_text_input_css_class:
"CSS classes for generated `input` tags of type `text`, `email` or `password`",
password_authentication_form_input_surround_css_class:
"CSS classes for the div surrounding an `label`/`input` combination.",
password_authentication_form_h2_css_class: "CSS classes for any form `h2` headers.",
password_authentication_form_submit_css_class: "CSS classes for any form submit buttons.",
password_authentication_form_css_class: "CSS classes for any `form` tags.",
password_authentication_form_error_ul_css_class: "CSS classes for `ul` tags in form errors",
password_authentication_form_error_li_css_class: "CSS classes for `li` tags in form errors",
password_authentication_box_css_class:
"CSS classes for the root `div` element in the `AshAuthentication.Phoenix.Components.PasswordAuthentication` component.",
password_authentication_box_spacer_css_class:
"CSS classes for the \"spacer\" in the `AshAuthentication.Phoenix.Components.PasswordAuthentication` component - if enabled.",
sign_in_box_css_class:
"CSS classes for the root `div` element in the `AshAuthentication.Phoenix.Components.SignIn` component.",
sign_in_row_css_class:
"CSS classes for each row in the `AshAuthentication.Phoenix.Components.SignIn` component.",
sign_in_live_css_class:
"CSS classes for the root element of the `AshAuthentication.Phoenix.SignInLive` live view."
]
@moduledoc """
Behaviour for overriding component styles and attributes in your application.
The default implementation is `AshAuthentication.Phoenix.Overrides.Default`
which uses [TailwindCSS](https://tailwindcss.com/) to generate a fairly
generic looking user interface.
You can override by setting the following in your `config.exs`:
```elixir
config :my_app, AshAuthentication.Phoenix, override_module: MyAppWeb.AuthOverrides
```
and defining `lib/my_app_web/auth_styles.ex` within which you can set CSS
classes for any values you want.
The `use` macro defines overridable versions of all callbacks which return
`nil`, so you only need to define the functions that you care about.
```elixir
defmodule MyAppWeb.AuthOverrides do
use AshAuthentication.Phoenix.Overrides
def password_authentication_form_label_css_class, do: "my-custom-css-class"
end
```
## Configuration
#{Enum.map(@configurables, &" * `#{elem(&1, 0)}` - #{elem(&1, 1)}\n")}
"""
alias __MODULE__
for {name, doc} <- @configurables do
Module.put_attribute(__MODULE__, :doc, {__ENV__.line, doc})
@callback unquote({name, [], Elixir}) :: nil | String.t()
end
@doc false
@spec __using__(any) :: Macro.t()
defmacro __using__(_) do
quote do
require Overrides
@behaviour Overrides
Overrides.generate_default_implementations()
Overrides.make_overridable()
end
end
@doc false
@spec generate_default_implementations :: Macro.t()
defmacro generate_default_implementations do
for {name, doc} <- @configurables do
quote do
@impl true
@doc unquote(doc)
def unquote({name, [], Elixir}), do: nil
end
end
end
@doc false
@spec make_overridable :: Macro.t()
defmacro make_overridable do
callbacks =
@configurables
|> Enum.map(&put_elem(&1, 1, 0))
quote do
defoverridable unquote(callbacks)
end
end
end

View file

@ -0,0 +1,69 @@
defmodule AshAuthentication.Phoenix.Overrides.Default do
@moduledoc """
The default implmentation of `AshAuthentication.Phoenix.Overrides` using
[TailwindCSS](https://tailwindcss.com/).
These colours and styles were chosen to be reasonably generic looking.
"""
use AshAuthentication.Phoenix.Overrides
@doc false
@impl true
def password_authentication_form_label_css_class,
do: "block text-sm font-medium text-gray-700 mb-1"
@doc false
@impl true
def password_authentication_form_input_surround_css_class, do: "mt-2 mb-2"
@doc false
@impl true
def password_authentication_form_text_input_css_class,
do: """
appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md
shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-pale-500
focus:border-blue-pale-500 sm:text-sm
"""
@doc false
@impl true
def password_authentication_form_h2_css_class,
do: "mt-2 mb-2 text-2xl tracking-tight font-bold text-gray-900"
@doc false
@impl true
def password_authentication_form_submit_css_class,
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
"""
@doc false
@impl true
def password_authentication_box_css_class, do: "mt-4 mb-4"
@doc false
@impl true
def password_authentication_box_spacer_css_class,
do: "w-full text-center font-semibold text-gray-400 uppercase text-lg"
@doc false
@impl true
def password_authentication_form_error_ul_css_class, do: "text-red-700 font-light"
@doc false
@impl true
def sign_in_box_css_class,
do: "flex-1 flex flex-col justify-center py-12 px-4 sm:px-6 lg:flex-none lg:px-20 xl:px-24"
@doc false
@impl true
def sign_in_row_css_class, do: "mx-auth w-full max-w-sm lg:w-96"
@doc false
@impl true
def sign_in_live_css_class, do: "grid place-items-center"
end

View file

@ -0,0 +1,52 @@
defmodule AshAuthentication.Phoenix.Plug do
@moduledoc """
Helper plugs mixed in to your router.
When you `use AshAuthentication.Phoenix.Router` this module is included, so
that you can use these plugs in your pipelines.
"""
alias AshAuthentication.Plug.Helpers
alias Plug.Conn
@doc """
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.
"""
@spec load_from_session(Conn.t(), any) :: Conn.t()
def load_from_session(conn, _opts) do
:otp_app
|> conn.private.phoenix_endpoint.config()
|> then(&Helpers.retrieve_from_session(conn, &1))
end
@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.
"""
@spec load_from_bearer(Conn.t(), any) :: Conn.t()
def load_from_bearer(conn, _opts) do
otp_app = conn.private.phoenix_endpoint.config(:otp_app)
Helpers.retrieve_from_bearer(conn, otp_app)
end
@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.
"""
@spec revoke_bearer_tokens(Conn.t(), any) :: Conn.t()
def revoke_bearer_tokens(conn, _opts) do
otp_app = conn.private.phoenix_endpoint.config(:otp_app)
Helpers.revoke_bearer_tokens(conn, otp_app)
end
@doc """
Store the actor in the connections' session.
"""
@spec store_in_session(Conn.t(), Ash.Resource.record()) :: Conn.t()
defdelegate store_in_session(conn, actor), to: AshAuthentication.Plug.Helpers
end

View file

@ -0,0 +1,112 @@
defmodule AshAuthentication.Phoenix.Router do
@moduledoc """
Phoenix route generation for AshAuthentication.
Using this module imports the macros in this module and the plug functions
from `AshAuthentication.Phoenix.Plug`.
## Usage
Adding authentication to your live-view router is very simple:
```elixir
defmodule MyAppWeb.Router do
use MyAppWeb, :router
use AshAuthentication.Phoenix.Router
pipeline :browser do
# ...
plug(:load_from_session)
end
pipeline :api do
# ...
plug(:load_from_bearer)
end
scope "/", MyAppWeb do
pipe_through :browser
sign_in_route
sign_out_route AuthController
auth_routes AuthController
end
```
"""
require Logger
@doc false
@spec __using__(any) :: Macro.t()
defmacro __using__(_opts) do
quote do
import AshAuthentication.Phoenix.Router
import AshAuthentication.Phoenix.Plug
end
end
@doc """
Generates the routes needed for the various subjects and providers
authenticating with AshAuthentication.
This is required if you wish to use authentication.
"""
defmacro auth_routes(auth_controller, path \\ "auth", opts \\ []) do
opts =
opts
|> Keyword.put_new(:as, :auth)
quote do
scope unquote(path), unquote(opts) do
match(:*, "/:subject_name/:provider", unquote(auth_controller), :request, as: :request)
match(:*, "/:subject_name/:provider/callback", unquote(auth_controller), :callback,
as: :callback
)
end
end
end
@doc """
Generates a generic, white-label sign-in page using LiveView and the
components in `AshAuthentication.Phoenix.Components`.
This is completely optional.
"""
defmacro sign_in_route(
path \\ "/sign-in",
live_view \\ AshAuthentication.Phoenix.SignInLive,
opts \\ []
) do
{as, opts} = Keyword.pop(opts, :as, :auth)
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 :sign_in do
live("/", unquote(live_view), :sign_in, as: unquote(as))
end
end
end
end
@doc """
Generates a sign-out route which points to the `sign_out` action in your auth
controller.
This is optional, but you probably want it.
"""
defmacro sign_out_route(auth_controller, path \\ "/sign-out", opts \\ []) do
{as, opts} = Keyword.pop(opts, :as, :auth)
quote do
scope unquote(path), unquote(opts) do
get("/", unquote(auth_controller), :sign_out, as: unquote(as))
end
end
end
end

View file

@ -0,0 +1,59 @@
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
using `Phoenix.LiveView.Controller.live_render/3` directly in your markup).
This live-view finds all Ash resources with an authentication configuration
(via `AshAuthentication.authenticated_resources/1`) and renders the
appropriate UI for their providers using
`AshAuthentication.Phoenix.Components.SignIn`.
## Overrides
See `AshAuthentication.Phoenix.Overrides` for more information.
* `sign_in_live_css_class` - applied to the root `div` element of this
liveview.
"""
use Phoenix.LiveView
alias AshAuthentication.Phoenix.Components
alias Phoenix.LiveView.{Rendered, Socket}
import Components.Helpers
@doc false
@impl true
@spec mount(map, map, Socket.t()) :: {:ok, Socket.t()}
def mount(_params, _, socket) do
resources =
socket
|> otp_app_from_socket()
|> AshAuthentication.authenticated_resources()
|> Enum.group_by(& &1.subject_name)
|> Enum.sort_by(&elem(&1, 0))
socket =
socket
|> assign(:resources, resources)
{:ok, socket}
end
@doc false
@impl true
@spec render(Socket.assigns()) :: Rendered.t()
def render(assigns) do
~H"""
<div class={override_for(@socket, :sign_in_live_css_class)}>
<%= for {subject_name, configs} <- @resources do %>
<%= for config <- configs do %>
<.live_component module={Components.SignIn} id={"sign-in-#{subject_name}"} config={config} />
<% end %>
<% end %>
</div>
"""
end
end

40
mix.exs
View file

@ -1,4 +1,5 @@
defmodule AshAuthenticationPhoenix.MixProject do
defmodule AshAuthentication.Phoenix.MixProject do
@moduledoc false
use Mix.Project
@version "0.1.0"
@ -15,9 +16,35 @@ defmodule AshAuthenticationPhoenix.MixProject do
package: package(),
elixirc_paths: elixirc_paths(Mix.env()),
dialyzer: [
plt_add_apps: [:mix, :ex_unit],
plt_add_apps: [:mix, :ex_unit, :ash_authentication],
plt_core_path: "priv/plts",
plt_file: {:no_warn, "priv/plts/dialyzer.plt"}
],
docs: [
main: "readme",
extras: ["README.md"],
formatters: ["html"],
filter_modules: ~r/^Elixir.AshAuthentication.Phoenix/,
source_url_pattern:
"https://github.com/team-alembic/ash_authentication_phoenix/blob/main/%{path}#L%{line}",
groups_for_modules: [
"Routing and Controller": [
AshAuthentication.Phoenix.Controller,
AshAuthentication.Phoenix.Plug,
AshAuthentication.Phoenix.Router
],
Customisation: [
AshAuthentication.Phoenix.SignInLive,
AshAuthentication.Phoenix.Overrides,
AshAuthentication.Phoenix.Overrides.Default,
AshAuthentication.Phoenix.Components.SignIn,
AshAuthentication.Phoenix.Components.PasswordAuthentication,
AshAuthentication.Phoenix.Components.PasswordAuthentication.SignInForm,
AshAuthentication.Phoenix.Components.PasswordAuthentication.RegisterForm,
AshAuthentication.Phoenix.Components.PasswordAuthentication.Input,
AshAuthentication.Phoenix.Components.Helpers
]
]
]
]
end
@ -31,7 +58,8 @@ defmodule AshAuthenticationPhoenix.MixProject do
links: %{
"Source" => "https://github.com/team-alembic/ash_authentication_phoenix"
},
source_url: "https://github.com/team-alembic/ash_authentication_phoenix"
source_url: "https://github.com/team-alembic/ash_authentication_phoenix",
files: ~w(lib .formatter.exs mix.exs README* LICENSE* CHANGELOG*)
]
end
@ -53,8 +81,7 @@ defmodule AshAuthenticationPhoenix.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:ash_authentication,
github: "team-alembic/ash_authentication", optional: true, branch: "feat/identity"},
{:ash_authentication, github: "team-alembic/ash_authentication", optional: true},
{:ash_phoenix, "~> 1.1"},
{:ash, "~> 2.2"},
{:jason, "~> 1.0"},
@ -83,8 +110,7 @@ defmodule AshAuthenticationPhoenix.MixProject do
"dialyzer",
"hex.audit",
"test"
],
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]
]
]
end

View file

@ -1,6 +1,6 @@
%{
"ash": {:hex, :ash, "2.2.0", "4fdc0fef5afb3f5045b1ca4e1ccb139b9f703cbc7c21dc645e32ac9582b11f63", [: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.1 and >= 0.1.28", [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", "48eca587e7076fe4f8547e919c0712f081ce85e66c316f6f51dd2535ad046013"},
"ash_authentication": {:git, "https://github.com/team-alembic/ash_authentication.git", "335f4b97d8f0bbd13c117e82ded621bf2884669f", [branch: "feat/identity"]},
"ash_authentication": {:git, "https://github.com/team-alembic/ash_authentication.git", "809a0faa55fc29fe08c6586fb53643605b09d2a3", []},
"ash_phoenix": {:hex, :ash_phoenix, "1.1.0", "7e8da0d463d181f5ee7f029722ea54519947d08d2c154b29b8b88e06d02fabdf", [: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", "0f8cf588d845fbd827ff522b3102170e4e90a1a2de63743b1be5e37b535a75ec"},
"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"},
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},

View file

@ -1,8 +1,5 @@
defmodule AshAuthenticationPhoenixTest do
defmodule AshAuthentication.PhoenixTest do
@moduledoc false
use ExUnit.Case
doctest AshAuthenticationPhoenix
test "greets the world" do
assert AshAuthenticationPhoenix.hello() == :world
end
doctest AshAuthentication.Phoenix
end