separate account settings from shop settings
All checks were successful
deploy / deploy (push) Successful in 3m28s
All checks were successful
deploy / deploy (push) Successful in 3m28s
- Create dedicated /admin/account page for user account management - Move email, password, and 2FA settings from /admin/settings - Add Account link to top of admin sidebar navigation - Add TOTP-based two-factor authentication with NimbleTOTP - Add TOTP verification LiveView for login flow - Add AccountController for TOTP session management - Remove Advanced section from settings (duplicated in dev tools) - Remove user email from sidebar footer (replaced by Account link) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,7 @@ defmodule BerrypodWeb.Auth.Login do
|
||||
action={~p"/users/log-in"}
|
||||
phx-submit="submit_magic"
|
||||
>
|
||||
<input :if={@return_to} type="hidden" name="return_to" value={@return_to} />
|
||||
<.input
|
||||
readonly={!!@current_scope}
|
||||
field={f[:email]}
|
||||
@@ -64,6 +65,7 @@ defmodule BerrypodWeb.Auth.Login do
|
||||
phx-submit="submit_password"
|
||||
phx-trigger-action={@trigger_submit}
|
||||
>
|
||||
<input :if={@return_to} type="hidden" name="return_to" value={@return_to} />
|
||||
<.input
|
||||
readonly={!!@current_scope}
|
||||
field={f[:email]}
|
||||
@@ -103,18 +105,20 @@ defmodule BerrypodWeb.Auth.Login do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
def mount(params, _session, socket) do
|
||||
email =
|
||||
Phoenix.Flash.get(socket.assigns.flash, :email) ||
|
||||
get_in(socket.assigns, [:current_scope, Access.key(:user), Access.key(:email)])
|
||||
|
||||
form = to_form(%{"email" => email}, as: "user")
|
||||
return_to = params["return_to"]
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
form: form,
|
||||
trigger_submit: false,
|
||||
email_configured: Mailer.email_verified?()
|
||||
email_configured: Mailer.email_verified?(),
|
||||
return_to: return_to
|
||||
)}
|
||||
end
|
||||
|
||||
|
||||
90
lib/berrypod_web/live/auth/totp_verification.ex
Normal file
90
lib/berrypod_web/live/auth/totp_verification.ex
Normal file
@@ -0,0 +1,90 @@
|
||||
defmodule BerrypodWeb.Auth.TotpVerification do
|
||||
use BerrypodWeb, :live_view
|
||||
|
||||
alias Berrypod.Accounts
|
||||
|
||||
@impl true
|
||||
def mount(_params, session, socket) do
|
||||
user_id = session["totp_pending_user_id"]
|
||||
remember_me = session["totp_pending_remember_me"]
|
||||
|
||||
if user_id do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:user_id, user_id)
|
||||
|> assign(:remember_me, remember_me)
|
||||
|> assign(:form, to_form(%{"code" => ""}, as: :totp))
|
||||
|> assign(:trigger_submit, false)}
|
||||
else
|
||||
{:ok,
|
||||
socket
|
||||
|> put_flash(:error, "Session expired. Please log in again.")
|
||||
|> redirect(to: ~p"/users/log-in")}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash} current_scope={@current_scope}>
|
||||
<div class="setup-page">
|
||||
<div class="setup-header">
|
||||
<.header>
|
||||
Two-factor authentication
|
||||
<:subtitle>
|
||||
Enter the 6-digit code from your authenticator app.
|
||||
</:subtitle>
|
||||
</.header>
|
||||
</div>
|
||||
|
||||
<.form
|
||||
for={@form}
|
||||
id="totp_form"
|
||||
action={~p"/users/verify-totp"}
|
||||
phx-submit="verify"
|
||||
phx-trigger-action={@trigger_submit}
|
||||
>
|
||||
<input type="hidden" name="remember_me" value={to_string(@remember_me)} />
|
||||
|
||||
<.input
|
||||
field={@form[:code]}
|
||||
type="text"
|
||||
label="Verification code"
|
||||
placeholder="000000"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
autocomplete="one-time-code"
|
||||
maxlength="6"
|
||||
autofocus
|
||||
phx-mounted={JS.focus()}
|
||||
/>
|
||||
|
||||
<.button variant="primary" class="admin-btn-block">
|
||||
Verify <span aria-hidden="true">→</span>
|
||||
</.button>
|
||||
</.form>
|
||||
|
||||
<p class="setup-footer admin-text-tertiary">
|
||||
Lost your device? Enter one of your backup codes instead.
|
||||
</p>
|
||||
</div>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("verify", %{"totp" => %{"code" => code}}, socket) do
|
||||
user = Accounts.get_user!(socket.assigns.user_id)
|
||||
|
||||
case Accounts.verify_totp(user, code) do
|
||||
:ok ->
|
||||
{:noreply, assign(socket, :trigger_submit, true)}
|
||||
|
||||
:error ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:form, to_form(%{"code" => ""}, as: :totp))
|
||||
|> put_flash(:error, "Invalid code. Please try again.")}
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user