mirror of
https://github.com/team-alembic/ash_authentication_phoenix.git
synced 2024-09-19 04:53:56 +12:00
feat: UI refresh. (#3)
* Redesign the overrides system to be easier to reason about. * Redesign the password UI to be fancier.
This commit is contained in:
parent
a47204aec2
commit
e25a8bf397
19 changed files with 602 additions and 408 deletions
|
@ -1,19 +1,24 @@
|
|||
# Used by "mix format"
|
||||
[
|
||||
import_deps: [:ash, :ash_authentication, :phoenix],
|
||||
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
|
||||
]
|
||||
]
|
||||
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,
|
||||
set: 2
|
||||
]
|
||||
|
||||
[
|
||||
import_deps: [:ash, :ash_authentication, :phoenix, :phoenix_live_view],
|
||||
inputs: ["{mix,.formatter}.exs", "{dev,config,lib,test}/**/*.{heex,ex,exs}"],
|
||||
locals_without_parens: locals_without_parens,
|
||||
export: [
|
||||
locals_without_parens: locals_without_parens
|
||||
],
|
||||
plugins: [Phoenix.LiveView.HTMLFormatter]
|
||||
]
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
<h2>Sorry, no diggity</h2>
|
||||
|
||||
<p>
|
||||
Reason:
|
||||
<pre>
|
||||
Reason: <pre>
|
||||
<%= inspect @failure_reason %>
|
||||
</pre>
|
||||
</p>
|
||||
|
||||
<%= link "Home", to: Routes.page_path(@conn, :index) %>
|
||||
<%= link("Home", to: Routes.page_path(@conn, :index)) %>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<h2>Signed out</h2>
|
||||
|
||||
<%= link "Home", to: Routes.page_path(@conn, :index) %>
|
||||
<%= link("Home", to: Routes.page_path(@conn, :index)) %>
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
Welcome back <%= @current_user.email %>.
|
||||
|
||||
<%= link "Home", to: Routes.page_path(@conn, :index) %>
|
||||
Welcome back <%= @current_user.email %>. <%= link("Home", to: Routes.page_path(@conn, :index)) %>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<main class="container">
|
||||
<p class="alert alert-info" role="alert"
|
||||
phx-click="lv:clear-flash"
|
||||
phx-value-key="info"><%= live_flash(@flash, :info) %></p>
|
||||
<p class="alert alert-info" role="alert" phx-click="lv:clear-flash" phx-value-key="info">
|
||||
<%= live_flash(@flash, :info) %>
|
||||
</p>
|
||||
|
||||
<p class="alert alert-danger" role="alert"
|
||||
phx-click="lv:clear-flash"
|
||||
phx-value-key="error"><%= live_flash(@flash, :error) %></p>
|
||||
<p class="alert alert-danger" role="alert" phx-click="lv:clear-flash" phx-value-key="error">
|
||||
<%= live_flash(@flash, :error) %>
|
||||
</p>
|
||||
|
||||
<%= @inner_content %>
|
||||
</main>
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<%= csrf_meta_tag() %>
|
||||
<%= live_title_tag assigns[:page_title] || "Dev" %>
|
||||
<script defer src="https://cdn.tailwindcss.com"></script>
|
||||
<%= live_title_tag(assigns[:page_title] || "Dev") %>
|
||||
<script defer src="https://cdn.tailwindcss.com">
|
||||
</script>
|
||||
<script type="module" crossorigin>
|
||||
import 'https://cdn.jsdelivr.net/gh/phoenixframework/phoenix_html@v3.2.0/priv/static/phoenix_html.js';
|
||||
import { Socket } from 'https://cdn.jsdelivr.net/gh/phoenixframework/phoenix@v1.6.14/priv/static/phoenix.mjs';
|
||||
|
|
|
@ -6,4 +6,4 @@
|
|||
<h2>Please sign in</h2>
|
||||
|
||||
<%= link("Sign in", to: Routes.auth_path(@conn, :sign_in)) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
|
44
lib/ash_authentication_phoenix/components/banner.ex
Normal file
44
lib/ash_authentication_phoenix/components/banner.ex
Normal file
|
@ -0,0 +1,44 @@
|
|||
defmodule AshAuthentication.Phoenix.Components.Banner do
|
||||
use AshAuthentication.Phoenix.Overrides.Overridable,
|
||||
root_class: "CSS class for the root `div` element.",
|
||||
href_class: "CSS class for the `a` tag.",
|
||||
href_url: "A URL for the banner image to link to.",
|
||||
image_class: "CSS class for the `img` tag.",
|
||||
image_url: "A URL for the `img` `src` attribute.",
|
||||
text_class: "CSS class for the text `div`.",
|
||||
text: "Banner text"
|
||||
|
||||
@moduledoc """
|
||||
Renders a very simple banner at the top of the sign-in component.
|
||||
|
||||
#{AshAuthentication.Phoenix.Overrides.Overridable.generate_docs()}
|
||||
"""
|
||||
|
||||
use Phoenix.LiveComponent
|
||||
alias Phoenix.LiveView.{Rendered, Socket}
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
@spec render(Socket.assigns()) :: Rendered.t() | no_return
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class={override_for(@socket, :root_class)}>
|
||||
<%= case {override_for(@socket, :href_url), override_for(@socket, :image_url)} do %>
|
||||
<% {nil, img} -> %>
|
||||
<img class={override_for(@socket, :image_class)} src={img} />
|
||||
<% {hrf, img} -> %>
|
||||
<a class={override_for(@socket, :href_class)} href={hrf}>
|
||||
<img class={override_for(@socket, :image_class)} src={img} />
|
||||
</a>
|
||||
<% end %>
|
||||
<%= case override_for(@socket, :text) do %>
|
||||
<% text when is_binary(text) -> %>
|
||||
<div class={override_for(@socket, :text_class)}>
|
||||
<%= text %>
|
||||
</div>
|
||||
<% _ -> %>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
|
@ -2,7 +2,7 @@ defmodule AshAuthentication.Phoenix.Components.Helpers do
|
|||
@moduledoc """
|
||||
Helpers which are commonly needed inside the various components.
|
||||
"""
|
||||
|
||||
alias AshAuthentication.Phoenix.Overrides
|
||||
alias Phoenix.LiveView.Socket
|
||||
|
||||
@doc """
|
||||
|
@ -23,21 +23,4 @@ defmodule AshAuthentication.Phoenix.Components.Helpers do
|
|||
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
|
||||
|
|
39
lib/ash_authentication_phoenix/components/horizontal_rule.ex
Normal file
39
lib/ash_authentication_phoenix/components/horizontal_rule.ex
Normal file
|
@ -0,0 +1,39 @@
|
|||
defmodule AshAuthentication.Phoenix.Components.HorizontalRule do
|
||||
use AshAuthentication.Phoenix.Overrides.Overridable,
|
||||
root_class: "CSS class for the root `div` element.",
|
||||
hr_outer_class: "CSS class for the outer `div` element of the horizontal rule.",
|
||||
hr_inner_class: "CSS class for the inner `div` element of the horizontal rule.",
|
||||
text_outer_class: "CSS class for the outer `div` element of the text area.",
|
||||
text_inner_class: "CSS class for the inner `div` element of the text area.",
|
||||
text: "Text to display in front of the horizontal rule."
|
||||
|
||||
@moduledoc """
|
||||
A horizontal rule with text.
|
||||
|
||||
This component is pretty tailwind-specific, but I (@jimsynz) really wanted a
|
||||
certain look. If you think I'm wrong then please let me know.
|
||||
|
||||
#{AshAuthentication.Phoenix.Overrides.Overridable.generate_docs()}
|
||||
"""
|
||||
|
||||
use Phoenix.LiveComponent
|
||||
alias Phoenix.LiveView.{Rendered, Socket}
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
@spec render(Socket.assigns()) :: Rendered.t() | no_return
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class={override_for(@socket, :root_class)}>
|
||||
<div class={override_for(@socket, :hr_outer_class)} aria-hidden="true">
|
||||
<div class={override_for(@socket, :hr_inner_class)}></div>
|
||||
</div>
|
||||
<div class={override_for(@socket, :text_outer_class)}>
|
||||
<span class={override_for(@socket, :text_inner_class)}>
|
||||
<%= override_for(@socket, :text) %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
|
@ -1,5 +1,13 @@
|
|||
defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication do
|
||||
@default_debounce 750
|
||||
use AshAuthentication.Phoenix.Overrides.Overridable,
|
||||
root_class: "CSS class for the root `div` element.",
|
||||
hide_class: "CSS class to apply to hide an element.",
|
||||
show_first: "The form to show on first load. Either `:sign_in` or `:register`.",
|
||||
interstitial_class: "CSS class for the `div` element between the form and the button.",
|
||||
sign_in_toggle_text: "Toggle text to display when the sign in form is showing.",
|
||||
register_toggle_text: "Toggle text to display when the register form is showing.",
|
||||
toggler_class: "CSS class for the toggler `a` element."
|
||||
|
||||
@moduledoc """
|
||||
Generates sign in and registration forms for a resource.
|
||||
|
||||
|
@ -17,62 +25,94 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication do
|
|||
* `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.
|
||||
#{AshAuthentication.Phoenix.Overrides.Overridable.generate_docs()}
|
||||
"""
|
||||
|
||||
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]
|
||||
}
|
||||
alias Phoenix.LiveView.{JS, Rendered, Socket}
|
||||
|
||||
@doc false
|
||||
@spec render(props) :: Rendered.t() | no_return
|
||||
@spec render(Socket.assigns()) :: Rendered.t() | no_return
|
||||
def render(assigns) do
|
||||
config = assigns.config
|
||||
provider = assigns.provider
|
||||
sign_in_action = Info.sign_in_action_name!(assigns.config.resource)
|
||||
register_action = Info.register_action_name!(assigns.config.resource)
|
||||
|
||||
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)
|
||||
|> assign(:sign_in_action, sign_in_action)
|
||||
|> assign_new(:sign_in_id, fn ->
|
||||
"#{config.subject_name}_#{provider.provides}_#{sign_in_action}"
|
||||
end)
|
||||
|> assign(:register_action, register_action)
|
||||
|> assign_new(:register_id, fn ->
|
||||
"#{config.subject_name}_#{provider.provides}_#{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)
|
||||
|
||||
~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 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}
|
||||
id={@sign_in_id}
|
||||
provider={@provider}
|
||||
config={@config}
|
||||
label={false}
|
||||
>
|
||||
<div class={override_for(@socket, :interstitial_class)}>
|
||||
<.toggler
|
||||
socket={@socket}
|
||||
register_id={@register_id}
|
||||
sign_in_id={@sign_in_id}
|
||||
message={override_for(@socket, :sign_in_toggle_text)}
|
||||
/>
|
||||
</div>
|
||||
</.live_component>
|
||||
</div>
|
||||
<div id={"#{@register_id}-wrapper"} class={unless @show_first == :register, do: @hide_class}>
|
||||
<.live_component
|
||||
module={PasswordAuthentication.RegisterForm}
|
||||
id={@register_id}
|
||||
provider={@provider}
|
||||
config={@config}
|
||||
label={false}
|
||||
>
|
||||
<div class={override_for(@socket, :interstitial_class)}>
|
||||
<.toggler
|
||||
socket={@socket}
|
||||
register_id={@register_id}
|
||||
sign_in_id={@sign_in_id}
|
||||
message={override_for(@socket, :register_toggle_text)}
|
||||
/>
|
||||
</div>
|
||||
</.live_component>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def toggler(assigns) do
|
||||
~H"""
|
||||
<a
|
||||
href="#"
|
||||
phx-click={
|
||||
JS.toggle(to: "##{@register_id}-wrapper")
|
||||
|> JS.toggle(to: "##{@sign_in_id}-wrapper")
|
||||
}
|
||||
class={override_for(@socket, :toggler_class)}
|
||||
>
|
||||
<%= @message %>
|
||||
</a>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,14 @@
|
|||
defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
|
||||
use AshAuthentication.Phoenix.Overrides.Overridable,
|
||||
field_class: "CSS class for `div` elements surrounding the fields.",
|
||||
label_class: "CSS class for `label` elements.",
|
||||
input_class: "CSS class for text/password `input` elements.",
|
||||
input_class_with_error:
|
||||
"CSS class for text/password `input` elements when there is a validation error.",
|
||||
submit_class: "CSS class for the form submit `input` element.",
|
||||
error_ul: "CSS class for the `ul` element on error lists.",
|
||||
error_li: "CSS class for the `li` elements on error lists."
|
||||
|
||||
@moduledoc """
|
||||
Function components for dealing with form input during password authentication.
|
||||
|
||||
|
@ -7,6 +17,8 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
|
|||
These function components are consumed by
|
||||
`AshAuthentication.Phoenix.Components.PasswordAuthentication.SignInForm` and
|
||||
`AshAuthentication.Phoenix.Components.PasswordAuthentication.RegisterForm`.
|
||||
|
||||
#{AshAuthentication.Phoenix.Overrides.Overridable.generate_docs()}
|
||||
"""
|
||||
|
||||
use Phoenix.Component
|
||||
|
@ -14,7 +26,6 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
|
|||
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.
|
||||
|
@ -31,15 +42,6 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
|
|||
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(),
|
||||
|
@ -62,11 +64,18 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
|
|||
_ -> :text
|
||||
end)
|
||||
end)
|
||||
|> assign_new(:input_class, fn ->
|
||||
if has_error?(assigns.form, identity_field) do
|
||||
override_for(assigns.socket, :input_class_with_error)
|
||||
else
|
||||
override_for(assigns.socket, :input_class)
|
||||
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) %>
|
||||
<div class={override_for(@socket, :field_class)}>
|
||||
<%= label(@form, @identity_field, class: override_for(@socket, :label_class)) %>
|
||||
<%= text_input(@form, @identity_field, type: to_string(@input_type), class: @input_class) %>
|
||||
<.error socket={@socket} form={@form} field={@identity_field} />
|
||||
</div>
|
||||
"""
|
||||
|
@ -85,17 +94,6 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
|
|||
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(),
|
||||
|
@ -103,14 +101,26 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
|
|||
required(:form) => AshPhoenix.Form.t()
|
||||
}) :: Rendered.t() | no_return
|
||||
def password_field(assigns) do
|
||||
password_field = Info.password_field!(assigns.config.resource)
|
||||
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(:password_field, Info.password_field!(assigns.config.resource))
|
||||
|> assign(:password_field, password_field)
|
||||
|> assign_new(:input_class, fn ->
|
||||
if has_error?(assigns.form, password_field) do
|
||||
override_for(assigns.socket, :input_class_with_error)
|
||||
else
|
||||
override_for(assigns.socket, :input_class)
|
||||
end
|
||||
end)
|
||||
|
||||
~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) %>
|
||||
<div class={override_for(@socket, :field_class)}>
|
||||
<%= label(@form, @password_field, class: override_for(@socket, :label_class)) %>
|
||||
<%= password_input(@form, @password_field,
|
||||
class: @input_class,
|
||||
value: input_value(@form, @password_field)
|
||||
) %>
|
||||
<.error socket={@socket} form={@form} field={@password_field} />
|
||||
</div>
|
||||
"""
|
||||
|
@ -129,16 +139,6 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
|
|||
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(),
|
||||
|
@ -146,17 +146,26 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
|
|||
required(:form) => AshPhoenix.Form.t()
|
||||
}) :: Rendered.t() | no_return
|
||||
def password_confirmation_field(assigns) do
|
||||
password_confirmation_field = Info.password_confirmation_field!(assigns.config.resource)
|
||||
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(
|
||||
:password_confirmation_field,
|
||||
Info.password_confirmation_field!(assigns.config.resource)
|
||||
)
|
||||
|> assign(:password_confirmation_field, password_confirmation_field)
|
||||
|> assign_new(:input_class, fn ->
|
||||
if has_error?(assigns.form, password_confirmation_field) do
|
||||
override_for(assigns.socket, :input_class_with_error)
|
||||
else
|
||||
override_for(assigns.socket, :input_class)
|
||||
end
|
||||
end)
|
||||
|
||||
~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) %>
|
||||
<div class={override_for(@socket, :field_class)}>
|
||||
<%= label(@form, @password_confirmation_field, class: override_for(@socket, :label_class)) %>
|
||||
<%= password_input(@form, @password_confirmation_field,
|
||||
class: @input_class,
|
||||
value: input_value(@form, @password_confirmation_field)
|
||||
) %>
|
||||
<.error socket={@socket} form={@form} field={@password_confirmation_field} />
|
||||
</div>
|
||||
"""
|
||||
|
@ -180,12 +189,6 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
|
|||
* `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(),
|
||||
|
@ -211,7 +214,7 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
|
|||
end)
|
||||
|
||||
~H"""
|
||||
<%= submit @label, class: override_for(@socket, :password_authentication_form_submit_css_class) %>
|
||||
<%= submit(@label, class: override_for(@socket, :submit_class)) %>
|
||||
"""
|
||||
end
|
||||
|
||||
|
@ -227,13 +230,6 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
|
|||
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(),
|
||||
|
@ -254,9 +250,9 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
|
|||
|
||||
~H"""
|
||||
<%= if Enum.any?(@errors) do %>
|
||||
<ul class={override_for(@socket, :password_authentication_form_error_ul_css_class)}>
|
||||
<ul class={override_for(@socket, :error_ul)}>
|
||||
<%= for error <- @errors do %>
|
||||
<li class={override_for(@socket, :password_authentication_form_error_li_css_class)} phx-feedback-for={input_name(@form, @field)}>
|
||||
<li class={override_for(@socket, :error_li)} phx-feedback-for={input_name(@form, @field)}>
|
||||
<%= @field_label %> <%= error %>
|
||||
</li>
|
||||
<% end %>
|
||||
|
@ -264,4 +260,10 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.Input do
|
|||
<% end %>
|
||||
"""
|
||||
end
|
||||
|
||||
defp has_error?(form, field) do
|
||||
form
|
||||
|> Form.errors()
|
||||
|> Keyword.has_key?(field)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.RegisterForm do
|
||||
@default_debounce 750
|
||||
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.",
|
||||
slot_class: "CSS class for the `div` surrounding the slot."
|
||||
|
||||
@moduledoc """
|
||||
Generates a default registration form.
|
||||
|
@ -24,16 +28,8 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.RegisterFo
|
|||
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.
|
||||
#{AshAuthentication.Phoenix.Overrides.Overridable.generate_docs()}
|
||||
"""
|
||||
|
||||
use Phoenix.LiveComponent
|
||||
|
@ -44,16 +40,9 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.RegisterFo
|
|||
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()}
|
||||
@spec update(Socket.assigns(), Socket.t()) :: {:ok, Socket.t()}
|
||||
def update(assigns, socket) do
|
||||
config = assigns.config
|
||||
action = Info.register_action_name!(config.resource)
|
||||
|
@ -73,43 +62,65 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.RegisterFo
|
|||
|> assign(assigns)
|
||||
|> assign(form: form, trigger_action: false, confirm?: confirm?)
|
||||
|> assign_new(:label, fn -> humanize(action) end)
|
||||
|> assign_new(:debounce, fn -> @default_debounce end)
|
||||
|> assign_new(:inner_block, fn -> nil end)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
@spec render(props) :: Rendered.t() | no_return
|
||||
@spec render(Socket.assigns()) :: Rendered.t() | no_return
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<div class={override_for(@socket, :root_class)}>
|
||||
<%= if @label do %>
|
||||
<h2 class={override_for(@socket, :password_authentication_form_h2_css_class)}><%= @label %></h2>
|
||||
<h2 class={override_for(@socket, :label_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)}>
|
||||
:let={form}
|
||||
for={@form}
|
||||
phx-submit="submit"
|
||||
phx-trigger-action={@trigger_action}
|
||||
phx-target={@myself}
|
||||
action={
|
||||
route_helpers(@socket).auth_callback_path(
|
||||
@socket.endpoint,
|
||||
:callback,
|
||||
@config.subject_name,
|
||||
@provider.provides
|
||||
)
|
||||
}
|
||||
method="POST"
|
||||
class={override_for(@socket, :form_class)}
|
||||
>
|
||||
<%= hidden_input(form, :action, value: "register") %>
|
||||
|
||||
<%= 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} />
|
||||
<PasswordAuthentication.Input.identity_field socket={@socket} config={@config} form={form} />
|
||||
<PasswordAuthentication.Input.password_field socket={@socket} config={@config} form={form} />
|
||||
|
||||
<%= if @confirm? do %>
|
||||
<PasswordAuthentication.Input.password_confirmation_field socket={@socket} config={@config} form={f} />
|
||||
<PasswordAuthentication.Input.password_confirmation_field
|
||||
socket={@socket}
|
||||
config={@config}
|
||||
form={form}
|
||||
/>
|
||||
<% end %>
|
||||
|
||||
<PasswordAuthentication.Input.submit socket={@socket} config={@config} form={f} action={:register}/>
|
||||
<%= if @inner_block do %>
|
||||
<div class={override_for(@socket, :slot_class)}>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<PasswordAuthentication.Input.submit
|
||||
socket={@socket}
|
||||
config={@config}
|
||||
form={form}
|
||||
action={:register}
|
||||
/>
|
||||
</.form>
|
||||
</div>
|
||||
"""
|
||||
|
@ -119,20 +130,6 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.RegisterFo
|
|||
@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))
|
||||
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.SignInForm do
|
||||
@default_debounce 750
|
||||
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.",
|
||||
slot_class: "CSS class for the `div` surrounding the slot."
|
||||
|
||||
@moduledoc """
|
||||
Generates a default sign in form.
|
||||
|
@ -26,16 +30,8 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.SignInForm
|
|||
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.
|
||||
#{AshAuthentication.Phoenix.Overrides.Overridable.generate_docs()}
|
||||
"""
|
||||
|
||||
use Phoenix.LiveComponent
|
||||
|
@ -43,14 +39,13 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.SignInForm
|
|||
alias AshAuthentication.Phoenix.Components.PasswordAuthentication
|
||||
alias AshPhoenix.Form
|
||||
alias Phoenix.LiveView.{Rendered, Socket}
|
||||
import AshAuthentication.Phoenix.Components.Helpers, only: [route_helpers: 1]
|
||||
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()
|
||||
optional(:label) => String.t() | false
|
||||
}
|
||||
|
||||
@doc false
|
||||
|
@ -74,35 +69,55 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.SignInForm
|
|||
|> assign(assigns)
|
||||
|> assign(form: form, trigger_action: false)
|
||||
|> assign_new(:label, fn -> humanize(action) end)
|
||||
|> assign_new(:debounce, fn -> @default_debounce end)
|
||||
|> assign_new(:inner_block, fn -> nil end)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
@spec render(props) :: Rendered.t() | no_return
|
||||
@spec render(Socket.assigns()) :: Rendered.t() | no_return
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<h2 class={override_for(@socket, :password_authentication_form_h2_css_class)}><%= @label %></h2>
|
||||
<div class={override_for(@socket, :root_class)}>
|
||||
<%= if @label do %>
|
||||
<h2 class={override_for(@socket, :label_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)}>
|
||||
<.form
|
||||
:let={form}
|
||||
for={@form}
|
||||
phx-submit="submit"
|
||||
phx-trigger-action={@trigger_action}
|
||||
phx-target={@myself}
|
||||
action={
|
||||
route_helpers(@socket).auth_callback_path(
|
||||
@socket.endpoint,
|
||||
:callback,
|
||||
@config.subject_name,
|
||||
@provider.provides()
|
||||
)
|
||||
}
|
||||
method="POST"
|
||||
class={override_for(@socket, :form_class)}
|
||||
>
|
||||
<%= hidden_input(form, :action, value: "sign_in") %>
|
||||
|
||||
<%= hidden_input f, :action, value: "sign_in" %>
|
||||
<PasswordAuthentication.Input.identity_field socket={@socket} config={@config} form={form} />
|
||||
<PasswordAuthentication.Input.password_field socket={@socket} config={@config} form={form} />
|
||||
|
||||
<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}/>
|
||||
<%= if @inner_block do %>
|
||||
<div class={override_for(@socket, :slot_class)}>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<PasswordAuthentication.Input.submit
|
||||
socket={@socket}
|
||||
config={@config}
|
||||
form={form}
|
||||
action={:sign_in}
|
||||
/>
|
||||
</.form>
|
||||
</div>
|
||||
"""
|
||||
|
@ -112,17 +127,6 @@ defmodule AshAuthentication.Phoenix.Components.PasswordAuthentication.SignInForm
|
|||
@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)
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
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."
|
||||
|
||||
@moduledoc """
|
||||
Renders sign in mark-up for an authenticated resource.
|
||||
|
||||
|
@ -23,30 +27,54 @@ defmodule AshAuthentication.Phoenix.Components.SignIn do
|
|||
* `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.
|
||||
#{AshAuthentication.Phoenix.Overrides.Overridable.generate_docs()}
|
||||
"""
|
||||
|
||||
use Phoenix.LiveComponent
|
||||
alias AshAuthentication.Phoenix.Components
|
||||
alias Phoenix.LiveView.Rendered
|
||||
alias Phoenix.LiveView.{Rendered, Socket}
|
||||
import AshAuthentication.Phoenix.Components.Helpers
|
||||
|
||||
@type props :: %{required(:config) => AshAuthentication.resource_config()}
|
||||
@doc false
|
||||
@impl true
|
||||
@spec update(Socket.assigns(), Socket.t()) :: {:ok, Socket.t()}
|
||||
def update(assigns, 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(assigns)
|
||||
|> assign(:resources, resources)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec render(props) :: Rendered.t() | no_return
|
||||
@impl true
|
||||
@spec render(Socket.assigns()) :: 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>
|
||||
<div class={override_for(@socket, :root_class)}>
|
||||
<.live_component module={Components.Banner} socket={@socket} id="sign-in-banner" />
|
||||
|
||||
<%= for {_subject_name, configs} <- @resources do %>
|
||||
<%= for config <- configs do %>
|
||||
<%= for provider <- config.providers do %>
|
||||
<div class={override_for(@socket, :provider_class)}>
|
||||
<.live_component
|
||||
module={component_for_provider(provider)}
|
||||
id={provider_id(provider, config)}
|
||||
provider={provider}
|
||||
config={config}
|
||||
/>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
|
@ -60,6 +88,6 @@ defmodule AshAuthentication.Phoenix.Components.SignIn do
|
|||
|> then(&Module.concat(Components, &1))
|
||||
|
||||
defp provider_id(provider, config) do
|
||||
"sign-in-#{config.subject_name}-#{provider.provides()}"
|
||||
"sign-in-#{config.subject_name}-with-#{provider.provides()}"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,27 +1,4 @@
|
|||
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.
|
||||
|
||||
|
@ -32,7 +9,7 @@ defmodule AshAuthentication.Phoenix.Overrides do
|
|||
You can override by setting the following in your `config.exs`:
|
||||
|
||||
```elixir
|
||||
config :my_app, AshAuthentication.Phoenix, override_module: MyAppWeb.AuthOverrides
|
||||
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
|
||||
|
@ -45,55 +22,63 @@ defmodule AshAuthentication.Phoenix.Overrides do
|
|||
defmodule MyAppWeb.AuthOverrides do
|
||||
use AshAuthentication.Phoenix.Overrides
|
||||
|
||||
def password_authentication_form_label_css_class, do: "my-custom-css-class"
|
||||
override
|
||||
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()
|
||||
@doc """
|
||||
Retrieve the override for a specific component and selector.
|
||||
"""
|
||||
@spec override_for(otp_app :: atom, component :: module, selector :: atom) :: any
|
||||
def override_for(otp_app, component, selector)
|
||||
when is_atom(otp_app) and is_atom(component) and is_atom(selector) do
|
||||
otp_app
|
||||
|> Application.get_env(AshAuthentication.Phoenix, [])
|
||||
|> Keyword.get(:overrides, [__MODULE__.Default])
|
||||
|> Enum.find_value(fn module ->
|
||||
module.overrides()
|
||||
|> Map.get({component, selector})
|
||||
end)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec __using__(any) :: Macro.t()
|
||||
defmacro __using__(_) do
|
||||
defmacro __using__(_env) do
|
||||
quote do
|
||||
require Overrides
|
||||
@behaviour Overrides
|
||||
|
||||
Overrides.generate_default_implementations()
|
||||
Overrides.make_overridable()
|
||||
require AshAuthentication.Phoenix.Overrides
|
||||
import AshAuthentication.Phoenix.Overrides, only: :macros
|
||||
Module.register_attribute(__MODULE__, :override, accumulate: true)
|
||||
@component nil
|
||||
@before_compile AshAuthentication.Phoenix.Overrides
|
||||
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
|
||||
defmacro override(component, do: block) do
|
||||
quote do
|
||||
@component unquote(component)
|
||||
unquote(block)
|
||||
end
|
||||
end
|
||||
|
||||
defmacro set(selector, value) do
|
||||
quote do
|
||||
@override {@component, unquote(selector), unquote(value)}
|
||||
end
|
||||
end
|
||||
|
||||
defmacro __before_compile__(env) do
|
||||
overrides =
|
||||
env.module
|
||||
|> Module.get_attribute(:override, [])
|
||||
|> Map.new(fn {component, selector, value} -> {{component, selector}, value} end)
|
||||
|> Macro.escape()
|
||||
|
||||
quote do
|
||||
def overrides do
|
||||
unquote(overrides)
|
||||
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
|
||||
|
|
|
@ -1,69 +1,92 @@
|
|||
defmodule AshAuthentication.Phoenix.Overrides.Default do
|
||||
@moduledoc """
|
||||
The default implmentation of `AshAuthentication.Phoenix.Overrides` using
|
||||
[TailwindCSS](https://tailwindcss.com/).
|
||||
This is the default overrides for our component UI.
|
||||
|
||||
These colours and styles were chosen to be reasonably generic looking.
|
||||
The CSS styles are based on [TailwindCSS](https://tailwindcss.com/).
|
||||
"""
|
||||
|
||||
use AshAuthentication.Phoenix.Overrides
|
||||
alias AshAuthentication.Phoenix.{Components, SignInLive}
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
def password_authentication_form_label_css_class,
|
||||
do: "block text-sm font-medium text-gray-700 mb-1"
|
||||
override SignInLive do
|
||||
set :root_class, "grid h-screen place-items-center"
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
def password_authentication_form_input_surround_css_class, do: "mt-2 mb-2"
|
||||
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
|
||||
"""
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
def password_authentication_form_text_input_css_class,
|
||||
do: """
|
||||
set :provider_class, "mx-auth w-full max-w-sm lg:w-96"
|
||||
end
|
||||
|
||||
override Components.Banner do
|
||||
set :root_class, "w-full flex justify-center py-2"
|
||||
set :href_class, nil
|
||||
set :href_url, "/"
|
||||
set :image_class, nil
|
||||
set :image_url, "https://ash-hq.org/images/ash-framework-light.png"
|
||||
set :text_class, nil
|
||||
set :text, nil
|
||||
end
|
||||
|
||||
override Components.HorizontalRule do
|
||||
set :root_class, "relative"
|
||||
set :hr_outer_class, "absolute inset-0 flex items-center"
|
||||
set :hr_inner_class, "w-full border-t border-gray-300"
|
||||
set :text_outer_class, "relative flex justify-center text-sm"
|
||||
set :text_inner_class, "px-2 bg-white text-gray-400 font-medium"
|
||||
set :text, "or"
|
||||
end
|
||||
|
||||
override Components.PasswordAuthentication do
|
||||
set :root_class, "mt-4 mb-4"
|
||||
set :interstitial_class, "flex justify-center text-sm font-medium"
|
||||
set :toggler_class, "text-blue-500 hover:text-blue-600"
|
||||
set :sign_in_toggle_text, "Need an account?"
|
||||
set :register_toggle_text, "Already have an account?"
|
||||
set :show_first, :sign_in
|
||||
set :hide_class, "hidden"
|
||||
end
|
||||
|
||||
override Components.PasswordAuthentication.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
|
||||
set :slot_class, "my-4"
|
||||
end
|
||||
|
||||
override Components.PasswordAuthentication.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
|
||||
set :slot_class, "my-4"
|
||||
end
|
||||
|
||||
override Components.PasswordAuthentication.Input do
|
||||
set :field_class, "mt-2 mb-2"
|
||||
set :label_class, "block text-sm font-medium text-gray-700 mb-1"
|
||||
|
||||
set :input_class, """
|
||||
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
|
||||
set :input_class_with_error, """
|
||||
appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md
|
||||
shadow-sm placeholder-gray-400 focus:outline-none border-red-400 sm:text-sm
|
||||
"""
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
def password_authentication_box_css_class, do: "mt-4 mb-4"
|
||||
set :submit_class, """
|
||||
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_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"
|
||||
set :error_ul, "text-red-400 font-light my-3 italic text-sm"
|
||||
set :error_li, nil
|
||||
end
|
||||
end
|
||||
|
|
71
lib/ash_authentication_phoenix/overrides/overridable.ex
Normal file
71
lib/ash_authentication_phoenix/overrides/overridable.ex
Normal file
|
@ -0,0 +1,71 @@
|
|||
defmodule AshAuthentication.Phoenix.Overrides.Overridable do
|
||||
@moduledoc """
|
||||
Auto generates documentation and helpers for components.
|
||||
"""
|
||||
|
||||
alias AshAuthentication.Phoenix.{Components.Helpers, Overrides}
|
||||
alias Phoenix.LiveView.Socket
|
||||
|
||||
@doc false
|
||||
@spec __using__(keyword) :: Macro.t()
|
||||
defmacro __using__(opts) do
|
||||
overrides =
|
||||
opts
|
||||
|> Enum.filter(fn
|
||||
{name, value} when is_atom(name) and is_binary(value) -> true
|
||||
_ -> false
|
||||
end)
|
||||
|> Map.new()
|
||||
|> Macro.escape()
|
||||
|
||||
quote do
|
||||
require AshAuthentication.Phoenix.Overrides
|
||||
require AshAuthentication.Phoenix.Overrides.Overridable
|
||||
@overrides unquote(overrides)
|
||||
import AshAuthentication.Phoenix.Overrides.Overridable, only: :macros
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec generate_docs :: Macro.t()
|
||||
defmacro generate_docs do
|
||||
quote do
|
||||
"""
|
||||
## Overrides
|
||||
|
||||
This component provides the following overrides:
|
||||
|
||||
#{@overrides |> Enum.map(&" * `#{inspect(elem(&1, 0))}` - #{elem(&1, 1)}\n")}
|
||||
|
||||
See `AshAuthentication.Phoenix.Overrides` for more information.
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec override_for(Socket.t(), atom, any) :: any
|
||||
defmacro override_for(socket, selector, default \\ nil) do
|
||||
overrides =
|
||||
__CALLER__.module
|
||||
|> Module.get_attribute(:overrides, %{})
|
||||
|
||||
if Map.has_key?(overrides, selector) do
|
||||
quote do
|
||||
override =
|
||||
unquote(socket)
|
||||
|> Helpers.otp_app_from_socket()
|
||||
|> Overrides.override_for(__MODULE__, unquote(selector))
|
||||
|
||||
override || unquote(default)
|
||||
end
|
||||
else
|
||||
IO.warn(
|
||||
"Unknown override `#{inspect(selector)}` in component `#{inspect(__CALLER__.module)}"
|
||||
)
|
||||
|
||||
quote do
|
||||
unquote(default)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,4 +1,7 @@
|
|||
defmodule AshAuthentication.Phoenix.SignInLive do
|
||||
use AshAuthentication.Phoenix.Overrides.Overridable,
|
||||
root_class: "CSS class for the root `div` element."
|
||||
|
||||
@moduledoc """
|
||||
A generic, white-label sign-in page.
|
||||
|
||||
|
@ -11,48 +14,20 @@ defmodule AshAuthentication.Phoenix.SignInLive do
|
|||
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.
|
||||
#{AshAuthentication.Phoenix.Overrides.Overridable.generate_docs()}
|
||||
"""
|
||||
|
||||
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 class={override_for(@socket, :root_class)}>
|
||||
<.live_component module={Components.SignIn} id="sign-in" />
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue