File size: 5,838 Bytes
8da2481 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | defmodule PlausibleWeb.Components.TwoFactor do
@moduledoc """
Reusable components specific to 2FA
"""
use PlausibleWeb, :component
attr :text, :string, required: true
attr :scale, :integer, default: 4
def qr_code(assigns) do
qr_code =
assigns.text
|> EQRCode.encode()
|> EQRCode.svg(%{width: 160})
assigns = assign(assigns, :code, qr_code)
~H"""
{Phoenix.HTML.raw(@code)}
"""
end
attr :id, :string, default: "verify-button"
attr :form, :any, required: true
attr :field, :any, required: true
attr :class, :string, default: ""
attr :show_button?, :boolean, default: true
attr :autofocus?, :boolean, default: false
attr :error, :string, default: nil
def verify_2fa_input(assigns) do
assigns = assign(assigns, :field, assigns[:form][assigns[:field]])
~H"""
<div class={[@class, "flex flex-col gap-y-6 items-center"]}>
<div class="flex flex-col gap-y-2 w-full">
<.otp_input
field={@field}
length={6}
oninvalid={@show_button? && "document.getElementById('#{@id}').disabled = false"}
autofocus={@autofocus? && "autofocus"}
/>
<p :if={@error} class="text-xs text-red-500 text-center">
{@error}
</p>
</div>
<.button
:if={@show_button?}
type="submit"
id={@id}
mt?={false}
class="w-full [&>span.label-enabled]:block [&>span.label-disabled]:hidden [&[disabled]>span.label-enabled]:hidden [&[disabled]>span.label-disabled]:block"
>
<span class="label-enabled pointer-events-none">
Verify
</span>
<span class="label-disabled">
<.spinner class="inline-block h-5 w-5 mr-2 text-white dark:text-gray-400" /> Verifying...
</span>
</.button>
</div>
"""
end
attr :id, :string, required: true
attr :state_param, :string, required: true
attr :form_data, :any, required: true
attr :form_target, :string, required: true
attr :onsubmit, :string, default: nil
attr :title, :string, required: true
slot :icon, required: true
slot :inner_block, required: true
slot :buttons, required: true
def modal(assigns) do
~H"""
<div
id={@id}
x-cloak
x-show={@state_param}
x-on:keyup.escape.window={"#{@state_param} = false"}
class="fixed z-10 inset-0 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true"
>
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div
x-show={@state_param}
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="fixed inset-0 bg-gray-500/75 dark:bg-gray-800/75 transition-opacity"
aria-hidden="true"
x-on:click={"#{@state_param} = false"}
>
</div>
<!-- This element is to trick the browser into centering the modal contents. -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
​
</span>
<div
x-show={@state_param}
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
class="relative inline-block align-bottom bg-white dark:bg-gray-900 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
>
<.form :let={f} for={@form_data} action={@form_target} onsubmit={@onsubmit}>
<div class="bg-white dark:bg-gray-850 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="hidden sm:block absolute top-0 right-0 pt-4 pr-4">
<a
href="#"
x-on:click.prevent={"#{@state_param} = false"}
class="bg-white dark:bg-gray-800 rounded-md text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 focus:outline-hidden"
>
<span class="sr-only">Close</span>
<Heroicons.x_mark class="h-6 w-6" />
</a>
</div>
<div class="sm:flex sm:items-start">
<div class="mx-auto shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-green-100 sm:mx-0 sm:h-10 sm:w-10">
{render_slot(@icon)}
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left text-gray-900 dark:text-gray-100">
<h3 class="text-lg leading-6 font-medium" id="modal-title">
{@title}
</h3>
{render_slot(@inner_block, f)}
</div>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-850 px-4 py-3 sm:px-9 sm:flex sm:flex-row-reverse">
{render_slot(@buttons)}
<.button
type="button"
x-on:click={"#{@state_param} = false"}
class="w-full sm:w-auto mr-2"
theme="secondary"
>
Cancel
</.button>
</div>
</.form>
</div>
</div>
</div>
"""
end
end
|