consolidate settings into single admin page
Merge shop status, payments, products (Printify), account (email/password), and advanced (dashboard/error tracker links) into /admin/settings. Simplify Auth.Settings to a redirector for /users/settings and confirm-email tokens. Remove Providers from sidebar nav. Update all redirects and tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
26d3bd782a
commit
4514608c07
@ -61,14 +61,6 @@
|
|||||||
<.icon name="hero-paint-brush" class="size-5" /> Theme
|
<.icon name="hero-paint-brush" class="size-5" /> Theme
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<.link
|
|
||||||
navigate={~p"/admin/providers"}
|
|
||||||
class={admin_nav_active?(@current_path, "/admin/providers")}
|
|
||||||
>
|
|
||||||
<.icon name="hero-cube" class="size-5" /> Providers
|
|
||||||
</.link>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<.link
|
<.link
|
||||||
navigate={~p"/admin/settings"}
|
navigate={~p"/admin/settings"}
|
||||||
|
|||||||
@ -55,7 +55,7 @@ defmodule SimpleshopThemeWeb.UserSessionController do
|
|||||||
UserAuth.disconnect_sessions(expired_tokens)
|
UserAuth.disconnect_sessions(expired_tokens)
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_session(:user_return_to, ~p"/users/settings")
|
|> put_session(:user_return_to, ~p"/admin/settings")
|
||||||
|> create(params, "Password updated successfully!")
|
|> create(params, "Password updated successfully!")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -83,7 +83,7 @@ defmodule SimpleshopThemeWeb.Admin.Providers.Form do
|
|||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> put_flash(:info, "Connected to Printify!")
|
|> put_flash(:info, "Connected to Printify!")
|
||||||
|> push_navigate(to: ~p"/admin/providers")}
|
|> push_navigate(to: ~p"/admin/settings")}
|
||||||
|
|
||||||
{:error, %Ecto.Changeset{} = changeset} ->
|
{:error, %Ecto.Changeset{} = changeset} ->
|
||||||
{:noreply, assign(socket, form: to_form(changeset))}
|
{:noreply, assign(socket, form: to_form(changeset))}
|
||||||
@ -96,7 +96,7 @@ defmodule SimpleshopThemeWeb.Admin.Providers.Form do
|
|||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> put_flash(:info, "Settings saved")
|
|> put_flash(:info, "Settings saved")
|
||||||
|> push_navigate(to: ~p"/admin/providers")}
|
|> push_navigate(to: ~p"/admin/settings")}
|
||||||
|
|
||||||
{:error, %Ecto.Changeset{} = changeset} ->
|
{:error, %Ecto.Changeset{} = changeset} ->
|
||||||
{:noreply, assign(socket, form: to_form(changeset))}
|
{:noreply, assign(socket, form: to_form(changeset))}
|
||||||
|
|||||||
@ -1,18 +1,26 @@
|
|||||||
defmodule SimpleshopThemeWeb.Admin.Settings do
|
defmodule SimpleshopThemeWeb.Admin.Settings do
|
||||||
use SimpleshopThemeWeb, :live_view
|
use SimpleshopThemeWeb, :live_view
|
||||||
|
|
||||||
|
alias SimpleshopTheme.Accounts
|
||||||
|
alias SimpleshopTheme.Products
|
||||||
alias SimpleshopTheme.Settings
|
alias SimpleshopTheme.Settings
|
||||||
alias SimpleshopTheme.Stripe.Setup, as: StripeSetup
|
alias SimpleshopTheme.Stripe.Setup, as: StripeSetup
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
|
user = socket.assigns.current_scope.user
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, "Settings")
|
|> assign(:page_title, "Settings")
|
||||||
|> assign(:site_live, Settings.site_live?())
|
|> assign(:site_live, Settings.site_live?())
|
||||||
|> assign_stripe_state()}
|
|> assign_stripe_state()
|
||||||
|
|> assign_products_state()
|
||||||
|
|> assign_account_state(user)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# -- Stripe assigns --
|
||||||
|
|
||||||
defp assign_stripe_state(socket) do
|
defp assign_stripe_state(socket) do
|
||||||
has_key = Settings.has_secret?("stripe_api_key")
|
has_key = Settings.has_secret?("stripe_api_key")
|
||||||
has_signing = Settings.has_secret?("stripe_signing_secret")
|
has_signing = Settings.has_secret?("stripe_signing_secret")
|
||||||
@ -32,11 +40,58 @@ defmodule SimpleshopThemeWeb.Admin.Settings do
|
|||||||
|> assign(:stripe_has_signing_secret, has_signing)
|
|> assign(:stripe_has_signing_secret, has_signing)
|
||||||
|> assign(:connect_form, to_form(%{"api_key" => ""}, as: :stripe))
|
|> assign(:connect_form, to_form(%{"api_key" => ""}, as: :stripe))
|
||||||
|> assign(:secret_form, to_form(%{"signing_secret" => ""}, as: :webhook))
|
|> assign(:secret_form, to_form(%{"signing_secret" => ""}, as: :webhook))
|
||||||
|> assign(:advanced_open, false)
|
|> assign(:stripe_advanced_open, false)
|
||||||
|> assign(:connecting, false)
|
|> assign(:connecting, false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# -- Products assigns --
|
||||||
|
|
||||||
|
defp assign_products_state(socket) do
|
||||||
|
connections = Products.list_provider_connections()
|
||||||
|
|
||||||
|
connection_info =
|
||||||
|
case connections do
|
||||||
|
[conn | _] ->
|
||||||
|
product_count = Products.count_products_for_connection(conn.id)
|
||||||
|
%{connection: conn, product_count: product_count}
|
||||||
|
|
||||||
|
[] ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
assign(socket, :printify, connection_info)
|
||||||
|
end
|
||||||
|
|
||||||
|
# -- Account assigns --
|
||||||
|
|
||||||
|
defp assign_account_state(socket, user) do
|
||||||
|
email_changeset = Accounts.change_user_email(user, %{}, validate_unique: false)
|
||||||
|
password_changeset = Accounts.change_user_password(user, %{}, hash_password: false)
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(:current_email, user.email)
|
||||||
|
|> assign(:email_form, to_form(email_changeset))
|
||||||
|
|> assign(:password_form, to_form(password_changeset))
|
||||||
|
|> assign(:trigger_submit, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
# -- Events: shop status --
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
def handle_event("toggle_site_live", _params, socket) do
|
||||||
|
new_value = !socket.assigns.site_live
|
||||||
|
{:ok, _} = Settings.set_site_live(new_value)
|
||||||
|
|
||||||
|
message = if new_value, do: "Shop is now live", else: "Shop taken offline"
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:site_live, new_value)
|
||||||
|
|> put_flash(:info, message)}
|
||||||
|
end
|
||||||
|
|
||||||
|
# -- Events: Stripe --
|
||||||
|
|
||||||
def handle_event("connect_stripe", %{"stripe" => %{"api_key" => api_key}}, socket) do
|
def handle_event("connect_stripe", %{"stripe" => %{"api_key" => api_key}}, socket) do
|
||||||
if api_key == "" do
|
if api_key == "" do
|
||||||
{:noreply, put_flash(socket, :error, "Please enter your Stripe secret key")}
|
{:noreply, put_flash(socket, :error, "Please enter your Stripe secret key")}
|
||||||
@ -100,42 +155,124 @@ defmodule SimpleshopThemeWeb.Admin.Settings do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("toggle_site_live", _params, socket) do
|
def handle_event("toggle_stripe_advanced", _params, socket) do
|
||||||
new_value = !socket.assigns.site_live
|
{:noreply, assign(socket, :stripe_advanced_open, !socket.assigns.stripe_advanced_open)}
|
||||||
{:ok, _} = Settings.set_site_live(new_value)
|
end
|
||||||
|
|
||||||
message = if new_value, do: "Shop is now live", else: "Shop taken offline"
|
# -- Events: products --
|
||||||
|
|
||||||
|
def handle_event("sync", %{"id" => id}, socket) do
|
||||||
|
connection = Products.get_provider_connection!(id)
|
||||||
|
|
||||||
|
case Products.enqueue_sync(connection) do
|
||||||
|
{:ok, _job} ->
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign_products_state()
|
||||||
|
|> put_flash(:info, "Sync started for #{connection.name}")}
|
||||||
|
|
||||||
|
{:error, _reason} ->
|
||||||
|
{:noreply, put_flash(socket, :error, "Failed to start sync")}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("delete_connection", %{"id" => id}, socket) do
|
||||||
|
connection = Products.get_provider_connection!(id)
|
||||||
|
{:ok, _} = Products.delete_provider_connection(connection)
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> assign(:site_live, new_value)
|
|> assign_products_state()
|
||||||
|> put_flash(:info, message)}
|
|> put_flash(:info, "Provider connection deleted")}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("toggle_advanced", _params, socket) do
|
# -- Events: account --
|
||||||
{:noreply, assign(socket, :advanced_open, !socket.assigns.advanced_open)}
|
|
||||||
|
def handle_event("validate_email", %{"user" => user_params}, socket) do
|
||||||
|
email_form =
|
||||||
|
socket.assigns.current_scope.user
|
||||||
|
|> Accounts.change_user_email(user_params, validate_unique: false)
|
||||||
|
|> Map.put(:action, :validate)
|
||||||
|
|> to_form()
|
||||||
|
|
||||||
|
{:noreply, assign(socket, email_form: email_form)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event("update_email", %{"user" => user_params}, socket) do
|
||||||
|
user = socket.assigns.current_scope.user
|
||||||
|
|
||||||
|
unless Accounts.sudo_mode?(user) do
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> put_flash(:error, "Please log in again to change account settings.")
|
||||||
|
|> redirect(to: ~p"/users/log-in")}
|
||||||
|
else
|
||||||
|
case Accounts.change_user_email(user, user_params) do
|
||||||
|
%{valid?: true} = changeset ->
|
||||||
|
Accounts.deliver_user_update_email_instructions(
|
||||||
|
Ecto.Changeset.apply_action!(changeset, :insert),
|
||||||
|
user.email,
|
||||||
|
&url(~p"/users/settings/confirm-email/#{&1}")
|
||||||
|
)
|
||||||
|
|
||||||
|
info = "A link to confirm your email change has been sent to the new address."
|
||||||
|
{:noreply, put_flash(socket, :info, info)}
|
||||||
|
|
||||||
|
changeset ->
|
||||||
|
{:noreply, assign(socket, :email_form, to_form(changeset, action: :insert))}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("validate_password", %{"user" => user_params}, socket) do
|
||||||
|
password_form =
|
||||||
|
socket.assigns.current_scope.user
|
||||||
|
|> Accounts.change_user_password(user_params, hash_password: false)
|
||||||
|
|> Map.put(:action, :validate)
|
||||||
|
|> to_form()
|
||||||
|
|
||||||
|
{:noreply, assign(socket, password_form: password_form)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("update_password", %{"user" => user_params}, socket) do
|
||||||
|
user = socket.assigns.current_scope.user
|
||||||
|
|
||||||
|
unless Accounts.sudo_mode?(user) do
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> put_flash(:error, "Please log in again to change account settings.")
|
||||||
|
|> redirect(to: ~p"/users/log-in")}
|
||||||
|
else
|
||||||
|
case Accounts.change_user_password(user, user_params) do
|
||||||
|
%{valid?: true} = changeset ->
|
||||||
|
{:noreply, assign(socket, trigger_submit: true, password_form: to_form(changeset))}
|
||||||
|
|
||||||
|
changeset ->
|
||||||
|
{:noreply, assign(socket, password_form: to_form(changeset, action: :insert))}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# -- Render --
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="max-w-2xl">
|
<div class="max-w-2xl">
|
||||||
<.header>
|
<.header>
|
||||||
Settings
|
Settings
|
||||||
<:subtitle>Shop status, payment providers, and API keys</:subtitle>
|
|
||||||
</.header>
|
</.header>
|
||||||
|
|
||||||
|
<%!-- Shop status --%>
|
||||||
<section class="mt-10">
|
<section class="mt-10">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<h2 class="text-lg font-semibold">Shop status</h2>
|
<h2 class="text-lg font-semibold">Shop status</h2>
|
||||||
<%= if @site_live do %>
|
<%= if @site_live do %>
|
||||||
<span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-green-600/20 ring-inset">
|
<.status_pill color="green">
|
||||||
<.icon name="hero-check-circle-mini" class="size-3" /> Live
|
<.icon name="hero-check-circle-mini" class="size-3" /> Live
|
||||||
</span>
|
</.status_pill>
|
||||||
<% else %>
|
<% else %>
|
||||||
<span class="inline-flex items-center gap-1 rounded-full bg-zinc-50 px-2 py-1 text-xs font-medium text-zinc-600 ring-1 ring-zinc-500/10 ring-inset">
|
<.status_pill color="zinc">Offline</.status_pill>
|
||||||
Offline
|
|
||||||
</span>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-sm text-zinc-600">
|
<p class="mt-2 text-sm text-zinc-600">
|
||||||
@ -165,22 +302,21 @@ defmodule SimpleshopThemeWeb.Admin.Settings do
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<%!-- Payments --%>
|
||||||
<section class="mt-10">
|
<section class="mt-10">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<h2 class="text-lg font-semibold">Stripe</h2>
|
<h2 class="text-lg font-semibold">Payments</h2>
|
||||||
<%= case @stripe_status do %>
|
<%= case @stripe_status do %>
|
||||||
<% :connected -> %>
|
<% :connected -> %>
|
||||||
<span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-green-600/20 ring-inset">
|
<.status_pill color="green">
|
||||||
<.icon name="hero-check-circle-mini" class="size-3" /> Connected
|
<.icon name="hero-check-circle-mini" class="size-3" /> Connected
|
||||||
</span>
|
</.status_pill>
|
||||||
<% :connected_localhost -> %>
|
<% :connected_localhost -> %>
|
||||||
<span class="inline-flex items-center gap-1 rounded-full bg-amber-50 px-2 py-1 text-xs font-medium text-amber-700 ring-1 ring-amber-600/20 ring-inset">
|
<.status_pill color="amber">
|
||||||
<.icon name="hero-exclamation-triangle-mini" class="size-3" /> Dev mode
|
<.icon name="hero-exclamation-triangle-mini" class="size-3" /> Dev mode
|
||||||
</span>
|
</.status_pill>
|
||||||
<% :not_configured -> %>
|
<% :not_configured -> %>
|
||||||
<span class="inline-flex items-center gap-1 rounded-full bg-zinc-50 px-2 py-1 text-xs font-medium text-zinc-600 ring-1 ring-zinc-500/10 ring-inset">
|
<.status_pill color="zinc">Not connected</.status_pill>
|
||||||
Not connected
|
|
||||||
</span>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -194,10 +330,213 @@ defmodule SimpleshopThemeWeb.Admin.Settings do
|
|||||||
stripe_signing_secret_hint={@stripe_signing_secret_hint}
|
stripe_signing_secret_hint={@stripe_signing_secret_hint}
|
||||||
stripe_has_signing_secret={@stripe_has_signing_secret}
|
stripe_has_signing_secret={@stripe_has_signing_secret}
|
||||||
secret_form={@secret_form}
|
secret_form={@secret_form}
|
||||||
advanced_open={@advanced_open}
|
advanced_open={@stripe_advanced_open}
|
||||||
/>
|
/>
|
||||||
<% end %>
|
<% end %>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<%!-- Products --%>
|
||||||
|
<section class="mt-10">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<h2 class="text-lg font-semibold">Products</h2>
|
||||||
|
<%= if @printify do %>
|
||||||
|
<.status_pill color="green">
|
||||||
|
<.icon name="hero-check-circle-mini" class="size-3" /> Connected
|
||||||
|
</.status_pill>
|
||||||
|
<% else %>
|
||||||
|
<.status_pill color="zinc">Not connected</.status_pill>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= if @printify do %>
|
||||||
|
<.printify_connected printify={@printify} />
|
||||||
|
<% else %>
|
||||||
|
<div class="mt-4">
|
||||||
|
<p class="text-sm text-zinc-600">
|
||||||
|
Connect a print-on-demand provider to import products into your shop.
|
||||||
|
</p>
|
||||||
|
<div class="mt-4">
|
||||||
|
<.link
|
||||||
|
navigate={~p"/admin/providers/new"}
|
||||||
|
class="inline-flex items-center gap-2 rounded-md bg-zinc-900 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-zinc-700"
|
||||||
|
>
|
||||||
|
<.icon name="hero-plus-mini" class="size-4" /> Connect to Printify
|
||||||
|
</.link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<%!-- Account --%>
|
||||||
|
<section class="mt-10">
|
||||||
|
<h2 class="text-lg font-semibold">Account</h2>
|
||||||
|
|
||||||
|
<div class="mt-4 space-y-6">
|
||||||
|
<.form
|
||||||
|
for={@email_form}
|
||||||
|
id="email_form"
|
||||||
|
phx-submit="update_email"
|
||||||
|
phx-change="validate_email"
|
||||||
|
>
|
||||||
|
<.input
|
||||||
|
field={@email_form[:email]}
|
||||||
|
type="email"
|
||||||
|
label="Email"
|
||||||
|
autocomplete="username"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div class="mt-3">
|
||||||
|
<.button phx-disable-with="Saving...">Change email</.button>
|
||||||
|
</div>
|
||||||
|
</.form>
|
||||||
|
|
||||||
|
<div class="border-t border-zinc-200 pt-6">
|
||||||
|
<.form
|
||||||
|
for={@password_form}
|
||||||
|
id="password_form"
|
||||||
|
action={~p"/users/update-password"}
|
||||||
|
method="post"
|
||||||
|
phx-change="validate_password"
|
||||||
|
phx-submit="update_password"
|
||||||
|
phx-trigger-action={@trigger_submit}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
name={@password_form[:email].name}
|
||||||
|
type="hidden"
|
||||||
|
id="hidden_user_email"
|
||||||
|
autocomplete="username"
|
||||||
|
value={@current_email}
|
||||||
|
/>
|
||||||
|
<.input
|
||||||
|
field={@password_form[:password]}
|
||||||
|
type="password"
|
||||||
|
label="New password"
|
||||||
|
autocomplete="new-password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<.input
|
||||||
|
field={@password_form[:password_confirmation]}
|
||||||
|
type="password"
|
||||||
|
label="Confirm new password"
|
||||||
|
autocomplete="new-password"
|
||||||
|
/>
|
||||||
|
<div class="mt-3">
|
||||||
|
<.button phx-disable-with="Saving...">Change password</.button>
|
||||||
|
</div>
|
||||||
|
</.form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<%!-- Advanced --%>
|
||||||
|
<section class="mt-10 pb-10">
|
||||||
|
<h2 class="text-lg font-semibold">Advanced</h2>
|
||||||
|
|
||||||
|
<div class="mt-4 flex flex-col gap-2">
|
||||||
|
<.link href={~p"/admin/dashboard"} class="text-sm text-zinc-600 hover:text-zinc-900">
|
||||||
|
<.icon name="hero-chart-bar" class="size-4 inline" /> System dashboard
|
||||||
|
</.link>
|
||||||
|
<.link href={~p"/admin/errors"} class="text-sm text-zinc-600 hover:text-zinc-900">
|
||||||
|
<.icon name="hero-bug-ant" class="size-4 inline" /> Error tracker
|
||||||
|
</.link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
# -- Function components --
|
||||||
|
|
||||||
|
attr :color, :string, required: true
|
||||||
|
slot :inner_block, required: true
|
||||||
|
|
||||||
|
defp status_pill(assigns) do
|
||||||
|
classes =
|
||||||
|
case assigns.color do
|
||||||
|
"green" -> "bg-green-50 text-green-700 ring-green-600/20"
|
||||||
|
"amber" -> "bg-amber-50 text-amber-700 ring-amber-600/20"
|
||||||
|
"zinc" -> "bg-zinc-50 text-zinc-600 ring-zinc-500/10"
|
||||||
|
_ -> "bg-zinc-50 text-zinc-600 ring-zinc-500/10"
|
||||||
|
end
|
||||||
|
|
||||||
|
assigns = assign(assigns, :classes, classes)
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<span class={[
|
||||||
|
"inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs font-medium ring-1 ring-inset",
|
||||||
|
@classes
|
||||||
|
]}>
|
||||||
|
{render_slot(@inner_block)}
|
||||||
|
</span>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
attr :printify, :map, required: true
|
||||||
|
|
||||||
|
defp printify_connected(assigns) do
|
||||||
|
conn = assigns.printify.connection
|
||||||
|
|
||||||
|
assigns =
|
||||||
|
assigns
|
||||||
|
|> assign(:connection, conn)
|
||||||
|
|> assign(:product_count, assigns.printify.product_count)
|
||||||
|
|> assign(:syncing, conn.sync_status == "syncing")
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<div class="mt-4">
|
||||||
|
<dl class="text-sm">
|
||||||
|
<div class="flex gap-2 py-1">
|
||||||
|
<dt class="text-zinc-500 w-28 shrink-0">Provider</dt>
|
||||||
|
<dd class="text-zinc-700">Printify</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 py-1">
|
||||||
|
<dt class="text-zinc-500 w-28 shrink-0">Shop</dt>
|
||||||
|
<dd class="text-zinc-700">{@connection.name}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 py-1">
|
||||||
|
<dt class="text-zinc-500 w-28 shrink-0">Products</dt>
|
||||||
|
<dd class="text-zinc-700">{@product_count}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 py-1">
|
||||||
|
<dt class="text-zinc-500 w-28 shrink-0">Last synced</dt>
|
||||||
|
<dd class="text-zinc-700">
|
||||||
|
<%= if @connection.last_synced_at do %>
|
||||||
|
{format_relative_time(@connection.last_synced_at)}
|
||||||
|
<% else %>
|
||||||
|
<span class="text-amber-600">Never</span>
|
||||||
|
<% end %>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
phx-click="sync"
|
||||||
|
phx-value-id={@connection.id}
|
||||||
|
disabled={@syncing}
|
||||||
|
class="inline-flex items-center gap-1.5 rounded-md bg-zinc-100 px-3 py-1.5 text-sm font-medium text-zinc-700 hover:bg-zinc-200 ring-1 ring-zinc-300 ring-inset"
|
||||||
|
>
|
||||||
|
<.icon
|
||||||
|
name="hero-arrow-path"
|
||||||
|
class={if @syncing, do: "size-4 animate-spin", else: "size-4"}
|
||||||
|
/>
|
||||||
|
{if @syncing, do: "Syncing...", else: "Sync products"}
|
||||||
|
</button>
|
||||||
|
<.link
|
||||||
|
navigate={~p"/admin/providers/#{@connection.id}/edit"}
|
||||||
|
class="inline-flex items-center gap-1.5 rounded-md bg-zinc-100 px-3 py-1.5 text-sm font-medium text-zinc-700 hover:bg-zinc-200 ring-1 ring-zinc-300 ring-inset"
|
||||||
|
>
|
||||||
|
<.icon name="hero-cog-6-tooth" class="size-4" /> Settings
|
||||||
|
</.link>
|
||||||
|
<button
|
||||||
|
phx-click="delete_connection"
|
||||||
|
phx-value-id={@connection.id}
|
||||||
|
data-confirm="Disconnect from Printify? Your synced products will remain in your shop."
|
||||||
|
class="text-sm text-red-600 hover:text-red-800 px-2 py-1.5"
|
||||||
|
>
|
||||||
|
Disconnect
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
@ -291,7 +630,7 @@ defmodule SimpleshopThemeWeb.Admin.Settings do
|
|||||||
<% else %>
|
<% else %>
|
||||||
<div class="border-t border-zinc-200 pt-3">
|
<div class="border-t border-zinc-200 pt-3">
|
||||||
<button
|
<button
|
||||||
phx-click="toggle_advanced"
|
phx-click="toggle_stripe_advanced"
|
||||||
class="flex items-center gap-1 text-sm text-zinc-500 hover:text-zinc-700"
|
class="flex items-center gap-1 text-sm text-zinc-500 hover:text-zinc-700"
|
||||||
>
|
>
|
||||||
<.icon
|
<.icon
|
||||||
@ -334,4 +673,15 @@ defmodule SimpleshopThemeWeb.Admin.Settings do
|
|||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp format_relative_time(datetime) do
|
||||||
|
diff = DateTime.diff(DateTime.utc_now(), datetime, :second)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
diff < 60 -> "just now"
|
||||||
|
diff < 3600 -> "#{div(diff, 60)} min ago"
|
||||||
|
diff < 86400 -> "#{div(diff, 3600)} hours ago"
|
||||||
|
true -> "#{div(diff, 86400)} days ago"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,71 +1,9 @@
|
|||||||
defmodule SimpleshopThemeWeb.Auth.Settings do
|
defmodule SimpleshopThemeWeb.Auth.Settings do
|
||||||
use SimpleshopThemeWeb, :live_view
|
use SimpleshopThemeWeb, :live_view
|
||||||
|
|
||||||
on_mount {SimpleshopThemeWeb.UserAuth, :require_sudo_mode}
|
|
||||||
|
|
||||||
alias SimpleshopTheme.Accounts
|
alias SimpleshopTheme.Accounts
|
||||||
|
|
||||||
@impl true
|
# Confirm-email token: process it and redirect to admin settings
|
||||||
def render(assigns) do
|
|
||||||
~H"""
|
|
||||||
<Layouts.app flash={@flash} current_scope={@current_scope}>
|
|
||||||
<div class="text-center">
|
|
||||||
<.header>
|
|
||||||
Account Settings
|
|
||||||
<:subtitle>Manage your account email address and password settings</:subtitle>
|
|
||||||
</.header>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<.form for={@email_form} id="email_form" phx-submit="update_email" phx-change="validate_email">
|
|
||||||
<.input
|
|
||||||
field={@email_form[:email]}
|
|
||||||
type="email"
|
|
||||||
label="Email"
|
|
||||||
autocomplete="username"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<.button variant="primary" phx-disable-with="Changing...">Change Email</.button>
|
|
||||||
</.form>
|
|
||||||
|
|
||||||
<div class="divider" />
|
|
||||||
|
|
||||||
<.form
|
|
||||||
for={@password_form}
|
|
||||||
id="password_form"
|
|
||||||
action={~p"/users/update-password"}
|
|
||||||
method="post"
|
|
||||||
phx-change="validate_password"
|
|
||||||
phx-submit="update_password"
|
|
||||||
phx-trigger-action={@trigger_submit}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
name={@password_form[:email].name}
|
|
||||||
type="hidden"
|
|
||||||
id="hidden_user_email"
|
|
||||||
autocomplete="username"
|
|
||||||
value={@current_email}
|
|
||||||
/>
|
|
||||||
<.input
|
|
||||||
field={@password_form[:password]}
|
|
||||||
type="password"
|
|
||||||
label="New password"
|
|
||||||
autocomplete="new-password"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<.input
|
|
||||||
field={@password_form[:password_confirmation]}
|
|
||||||
type="password"
|
|
||||||
label="Confirm new password"
|
|
||||||
autocomplete="new-password"
|
|
||||||
/>
|
|
||||||
<.button variant="primary" phx-disable-with="Saving...">
|
|
||||||
Save Password
|
|
||||||
</.button>
|
|
||||||
</.form>
|
|
||||||
</Layouts.app>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(%{"token" => token}, _session, socket) do
|
def mount(%{"token" => token}, _session, socket) do
|
||||||
socket =
|
socket =
|
||||||
@ -77,81 +15,16 @@ defmodule SimpleshopThemeWeb.Auth.Settings do
|
|||||||
put_flash(socket, :error, "Email change link is invalid or it has expired.")
|
put_flash(socket, :error, "Email change link is invalid or it has expired.")
|
||||||
end
|
end
|
||||||
|
|
||||||
{:ok, push_navigate(socket, to: ~p"/users/settings")}
|
{:ok, redirect(socket, to: ~p"/admin/settings")}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Main mount: just redirect — account settings live in admin now
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
user = socket.assigns.current_scope.user
|
{:ok, redirect(socket, to: ~p"/admin/settings")}
|
||||||
email_changeset = Accounts.change_user_email(user, %{}, validate_unique: false)
|
|
||||||
password_changeset = Accounts.change_user_password(user, %{}, hash_password: false)
|
|
||||||
|
|
||||||
socket =
|
|
||||||
socket
|
|
||||||
|> assign(:current_email, user.email)
|
|
||||||
|> assign(:email_form, to_form(email_changeset))
|
|
||||||
|> assign(:password_form, to_form(password_changeset))
|
|
||||||
|> assign(:trigger_submit, false)
|
|
||||||
|
|
||||||
{:ok, socket}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("validate_email", params, socket) do
|
def render(assigns) do
|
||||||
%{"user" => user_params} = params
|
~H""
|
||||||
|
|
||||||
email_form =
|
|
||||||
socket.assigns.current_scope.user
|
|
||||||
|> Accounts.change_user_email(user_params, validate_unique: false)
|
|
||||||
|> Map.put(:action, :validate)
|
|
||||||
|> to_form()
|
|
||||||
|
|
||||||
{:noreply, assign(socket, email_form: email_form)}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("update_email", params, socket) do
|
|
||||||
%{"user" => user_params} = params
|
|
||||||
user = socket.assigns.current_scope.user
|
|
||||||
true = Accounts.sudo_mode?(user)
|
|
||||||
|
|
||||||
case Accounts.change_user_email(user, user_params) do
|
|
||||||
%{valid?: true} = changeset ->
|
|
||||||
Accounts.deliver_user_update_email_instructions(
|
|
||||||
Ecto.Changeset.apply_action!(changeset, :insert),
|
|
||||||
user.email,
|
|
||||||
&url(~p"/users/settings/confirm-email/#{&1}")
|
|
||||||
)
|
|
||||||
|
|
||||||
info = "A link to confirm your email change has been sent to the new address."
|
|
||||||
{:noreply, socket |> put_flash(:info, info)}
|
|
||||||
|
|
||||||
changeset ->
|
|
||||||
{:noreply, assign(socket, :email_form, to_form(changeset, action: :insert))}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("validate_password", params, socket) do
|
|
||||||
%{"user" => user_params} = params
|
|
||||||
|
|
||||||
password_form =
|
|
||||||
socket.assigns.current_scope.user
|
|
||||||
|> Accounts.change_user_password(user_params, hash_password: false)
|
|
||||||
|> Map.put(:action, :validate)
|
|
||||||
|> to_form()
|
|
||||||
|
|
||||||
{:noreply, assign(socket, password_form: password_form)}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("update_password", params, socket) do
|
|
||||||
%{"user" => user_params} = params
|
|
||||||
user = socket.assigns.current_scope.user
|
|
||||||
true = Accounts.sudo_mode?(user)
|
|
||||||
|
|
||||||
case Accounts.change_user_password(user, user_params) do
|
|
||||||
%{valid?: true} = changeset ->
|
|
||||||
{:noreply, assign(socket, trigger_submit: true, password_form: to_form(changeset))}
|
|
||||||
|
|
||||||
changeset ->
|
|
||||||
{:noreply, assign(socket, password_form: to_form(changeset, action: :insert))}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -257,9 +257,9 @@ defmodule SimpleshopThemeWeb.UserAuth do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc "Returns the path to redirect to after log in."
|
@doc "Returns the path to redirect to after log in."
|
||||||
# the user was already logged in, redirect to settings
|
# the user was already logged in, redirect to admin settings
|
||||||
def signed_in_path(%Plug.Conn{assigns: %{current_scope: %Scope{user: %Accounts.User{}}}}) do
|
def signed_in_path(%Plug.Conn{assigns: %{current_scope: %Scope{user: %Accounts.User{}}}}) do
|
||||||
~p"/users/settings"
|
~p"/admin/settings"
|
||||||
end
|
end
|
||||||
|
|
||||||
def signed_in_path(_), do: ~p"/"
|
def signed_in_path(_), do: ~p"/"
|
||||||
|
|||||||
@ -21,7 +21,7 @@ defmodule SimpleshopThemeWeb.UserSessionControllerTest do
|
|||||||
assert redirected_to(conn) == ~p"/"
|
assert redirected_to(conn) == ~p"/"
|
||||||
|
|
||||||
# Now do a logged in request and assert on the page content
|
# Now do a logged in request and assert on the page content
|
||||||
conn = get(conn, ~p"/users/settings")
|
conn = get(conn, ~p"/admin/settings")
|
||||||
response = html_response(conn, 200)
|
response = html_response(conn, 200)
|
||||||
assert response =~ user.email
|
assert response =~ user.email
|
||||||
end
|
end
|
||||||
@ -83,7 +83,7 @@ defmodule SimpleshopThemeWeb.UserSessionControllerTest do
|
|||||||
assert redirected_to(conn) == ~p"/"
|
assert redirected_to(conn) == ~p"/"
|
||||||
|
|
||||||
# Now do a logged in request and assert on the page content
|
# Now do a logged in request and assert on the page content
|
||||||
conn = get(conn, ~p"/users/settings")
|
conn = get(conn, ~p"/admin/settings")
|
||||||
response = html_response(conn, 200)
|
response = html_response(conn, 200)
|
||||||
assert response =~ user.email
|
assert response =~ user.email
|
||||||
end
|
end
|
||||||
@ -105,7 +105,7 @@ defmodule SimpleshopThemeWeb.UserSessionControllerTest do
|
|||||||
assert Accounts.get_user!(user.id).confirmed_at
|
assert Accounts.get_user!(user.id).confirmed_at
|
||||||
|
|
||||||
# Now do a logged in request and assert on the page content
|
# Now do a logged in request and assert on the page content
|
||||||
conn = get(conn, ~p"/users/settings")
|
conn = get(conn, ~p"/admin/settings")
|
||||||
response = html_response(conn, 200)
|
response = html_response(conn, 200)
|
||||||
assert response =~ user.email
|
assert response =~ user.email
|
||||||
end
|
end
|
||||||
|
|||||||
@ -19,7 +19,6 @@ defmodule SimpleshopThemeWeb.Admin.LayoutTest do
|
|||||||
|
|
||||||
assert has_element?(view, ~s(a[href="/admin/orders"]), "Orders")
|
assert has_element?(view, ~s(a[href="/admin/orders"]), "Orders")
|
||||||
assert has_element?(view, ~s(a[href="/admin/theme"]), "Theme")
|
assert has_element?(view, ~s(a[href="/admin/theme"]), "Theme")
|
||||||
assert has_element?(view, ~s(a[href="/admin/providers"]), "Providers")
|
|
||||||
assert has_element?(view, ~s(a[href="/admin/settings"]), "Settings")
|
assert has_element?(view, ~s(a[href="/admin/settings"]), "Settings")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,9 @@ defmodule SimpleshopThemeWeb.Admin.SettingsTest do
|
|||||||
|
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
import SimpleshopTheme.AccountsFixtures
|
import SimpleshopTheme.AccountsFixtures
|
||||||
|
import SimpleshopTheme.ProductsFixtures
|
||||||
|
|
||||||
|
alias SimpleshopTheme.Accounts
|
||||||
alias SimpleshopTheme.Settings
|
alias SimpleshopTheme.Settings
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
@ -77,7 +79,7 @@ defmodule SimpleshopThemeWeb.Admin.SettingsTest do
|
|||||||
|
|
||||||
html =
|
html =
|
||||||
view
|
view
|
||||||
|> form("form", %{stripe: %{api_key: ""}})
|
|> form(~s(form[phx-submit="connect_stripe"]), %{stripe: %{api_key: ""}})
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
|
|
||||||
assert html =~ "Please enter your Stripe secret key"
|
assert html =~ "Please enter your Stripe secret key"
|
||||||
@ -106,7 +108,9 @@ defmodule SimpleshopThemeWeb.Admin.SettingsTest do
|
|||||||
|
|
||||||
html =
|
html =
|
||||||
view
|
view
|
||||||
|> form("form", %{webhook: %{signing_secret: "whsec_test_manual_456"}})
|
|> form(~s(form[phx-submit="save_signing_secret"]), %{
|
||||||
|
webhook: %{signing_secret: "whsec_test_manual_456"}
|
||||||
|
})
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
|
|
||||||
assert html =~ "Webhook signing secret saved"
|
assert html =~ "Webhook signing secret saved"
|
||||||
@ -118,7 +122,7 @@ defmodule SimpleshopThemeWeb.Admin.SettingsTest do
|
|||||||
|
|
||||||
html =
|
html =
|
||||||
view
|
view
|
||||||
|> form("form", %{webhook: %{signing_secret: ""}})
|
|> form(~s(form[phx-submit="save_signing_secret"]), %{webhook: %{signing_secret: ""}})
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
|
|
||||||
assert html =~ "Please enter a signing secret"
|
assert html =~ "Please enter a signing secret"
|
||||||
@ -135,4 +139,118 @@ defmodule SimpleshopThemeWeb.Admin.SettingsTest do
|
|||||||
refute Settings.has_secret?("stripe_api_key")
|
refute Settings.has_secret?("stripe_api_key")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "products section" do
|
||||||
|
setup %{conn: conn, user: user} do
|
||||||
|
conn = log_in_user(conn, user)
|
||||||
|
%{conn: conn}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "shows connect button when no provider connected", %{conn: conn} do
|
||||||
|
{:ok, view, html} = live(conn, ~p"/admin/settings")
|
||||||
|
|
||||||
|
assert html =~ "Products"
|
||||||
|
assert html =~ "Not connected"
|
||||||
|
assert has_element?(view, ~s(a[href="/admin/providers/new"]), "Connect to Printify")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "shows connection info when provider connected", %{conn: conn} do
|
||||||
|
conn_record = provider_connection_fixture(%{name: "Test Shop"})
|
||||||
|
product_fixture(%{provider_connection: conn_record})
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/admin/settings")
|
||||||
|
|
||||||
|
assert html =~ "Connected"
|
||||||
|
assert html =~ "Test Shop"
|
||||||
|
assert html =~ "Sync products"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "account section" do
|
||||||
|
setup %{conn: conn, user: user} do
|
||||||
|
conn = log_in_user(conn, user)
|
||||||
|
%{conn: conn, user: user}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders email and password forms", %{conn: conn, user: user} do
|
||||||
|
{:ok, view, html} = live(conn, ~p"/admin/settings")
|
||||||
|
|
||||||
|
assert html =~ "Account"
|
||||||
|
assert html =~ user.email
|
||||||
|
assert has_element?(view, "#email_form")
|
||||||
|
assert has_element?(view, "#password_form")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validates email change", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/settings")
|
||||||
|
|
||||||
|
result =
|
||||||
|
view
|
||||||
|
|> element("#email_form")
|
||||||
|
|> render_change(%{"user" => %{"email" => "with spaces"}})
|
||||||
|
|
||||||
|
assert result =~ "must have the @ sign and no spaces"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "submits email change", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/settings")
|
||||||
|
|
||||||
|
result =
|
||||||
|
view
|
||||||
|
|> form("#email_form", %{"user" => %{"email" => unique_user_email()}})
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert result =~ "A link to confirm your email"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validates password", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/settings")
|
||||||
|
|
||||||
|
result =
|
||||||
|
view
|
||||||
|
|> element("#password_form")
|
||||||
|
|> render_change(%{
|
||||||
|
"user" => %{
|
||||||
|
"password" => "short",
|
||||||
|
"password_confirmation" => "mismatch"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert result =~ "should be at least 12 character(s)"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "submits valid password change", %{conn: conn, user: user} do
|
||||||
|
new_password = valid_user_password()
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/settings")
|
||||||
|
|
||||||
|
form =
|
||||||
|
form(view, "#password_form", %{
|
||||||
|
"user" => %{
|
||||||
|
"email" => user.email,
|
||||||
|
"password" => new_password,
|
||||||
|
"password_confirmation" => new_password
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
render_submit(form)
|
||||||
|
new_password_conn = follow_trigger_action(form, conn)
|
||||||
|
|
||||||
|
assert redirected_to(new_password_conn) == ~p"/admin/settings"
|
||||||
|
assert Accounts.get_user_by_email_and_password(user.email, new_password)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "advanced section" do
|
||||||
|
setup %{conn: conn, user: user} do
|
||||||
|
conn = log_in_user(conn, user)
|
||||||
|
%{conn: conn}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "shows links to system tools", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/settings")
|
||||||
|
|
||||||
|
assert has_element?(view, ~s(a[href="/admin/dashboard"]), "System dashboard")
|
||||||
|
assert has_element?(view, ~s(a[href="/admin/errors"]), "Error tracker")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -5,159 +5,18 @@ defmodule SimpleshopThemeWeb.Auth.SettingsTest do
|
|||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
import SimpleshopTheme.AccountsFixtures
|
import SimpleshopTheme.AccountsFixtures
|
||||||
|
|
||||||
describe "Settings page" do
|
describe "settings redirect" do
|
||||||
test "renders settings page", %{conn: conn} do
|
test "redirects to admin settings when logged in", %{conn: conn} do
|
||||||
{:ok, _lv, html} =
|
conn = log_in_user(conn, user_fixture())
|
||||||
conn
|
assert {:error, {:redirect, %{to: "/admin/settings"}}} = live(conn, ~p"/users/settings")
|
||||||
|> log_in_user(user_fixture())
|
|
||||||
|> live(~p"/users/settings")
|
|
||||||
|
|
||||||
assert html =~ "Change Email"
|
|
||||||
assert html =~ "Save Password"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "redirects if user is not logged in", %{conn: conn} do
|
test "redirects to login when not logged in", %{conn: conn} do
|
||||||
assert {:error, redirect} = live(conn, ~p"/users/settings")
|
assert {:error, redirect} = live(conn, ~p"/users/settings")
|
||||||
|
|
||||||
assert {:redirect, %{to: path, flash: flash}} = redirect
|
assert {:redirect, %{to: path, flash: flash}} = redirect
|
||||||
assert path == ~p"/users/log-in"
|
assert path == ~p"/users/log-in"
|
||||||
assert %{"error" => "You must log in to access this page."} = flash
|
assert %{"error" => "You must log in to access this page."} = flash
|
||||||
end
|
end
|
||||||
|
|
||||||
test "redirects if user is not in sudo mode", %{conn: conn} do
|
|
||||||
{:ok, conn} =
|
|
||||||
conn
|
|
||||||
|> log_in_user(user_fixture(),
|
|
||||||
token_authenticated_at: DateTime.add(DateTime.utc_now(:second), -11, :minute)
|
|
||||||
)
|
|
||||||
|> live(~p"/users/settings")
|
|
||||||
|> follow_redirect(conn, ~p"/users/log-in")
|
|
||||||
|
|
||||||
assert conn.resp_body =~ "You must re-authenticate to access this page."
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "update email form" do
|
|
||||||
setup %{conn: conn} do
|
|
||||||
user = user_fixture()
|
|
||||||
%{conn: log_in_user(conn, user), user: user}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "updates the user email", %{conn: conn, user: user} do
|
|
||||||
new_email = unique_user_email()
|
|
||||||
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
|
||||||
|
|
||||||
result =
|
|
||||||
lv
|
|
||||||
|> form("#email_form", %{
|
|
||||||
"user" => %{"email" => new_email}
|
|
||||||
})
|
|
||||||
|> render_submit()
|
|
||||||
|
|
||||||
assert result =~ "A link to confirm your email"
|
|
||||||
assert Accounts.get_user_by_email(user.email)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "renders errors with invalid data (phx-change)", %{conn: conn} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
|
||||||
|
|
||||||
result =
|
|
||||||
lv
|
|
||||||
|> element("#email_form")
|
|
||||||
|> render_change(%{
|
|
||||||
"action" => "update_email",
|
|
||||||
"user" => %{"email" => "with spaces"}
|
|
||||||
})
|
|
||||||
|
|
||||||
assert result =~ "Change Email"
|
|
||||||
assert result =~ "must have the @ sign and no spaces"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "renders errors with invalid data (phx-submit)", %{conn: conn, user: user} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
|
||||||
|
|
||||||
result =
|
|
||||||
lv
|
|
||||||
|> form("#email_form", %{
|
|
||||||
"user" => %{"email" => user.email}
|
|
||||||
})
|
|
||||||
|> render_submit()
|
|
||||||
|
|
||||||
assert result =~ "Change Email"
|
|
||||||
assert result =~ "did not change"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "update password form" do
|
|
||||||
setup %{conn: conn} do
|
|
||||||
user = user_fixture()
|
|
||||||
%{conn: log_in_user(conn, user), user: user}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "updates the user password", %{conn: conn, user: user} do
|
|
||||||
new_password = valid_user_password()
|
|
||||||
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
|
||||||
|
|
||||||
form =
|
|
||||||
form(lv, "#password_form", %{
|
|
||||||
"user" => %{
|
|
||||||
"email" => user.email,
|
|
||||||
"password" => new_password,
|
|
||||||
"password_confirmation" => new_password
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
render_submit(form)
|
|
||||||
|
|
||||||
new_password_conn = follow_trigger_action(form, conn)
|
|
||||||
|
|
||||||
assert redirected_to(new_password_conn) == ~p"/users/settings"
|
|
||||||
|
|
||||||
assert get_session(new_password_conn, :user_token) != get_session(conn, :user_token)
|
|
||||||
|
|
||||||
assert Phoenix.Flash.get(new_password_conn.assigns.flash, :info) =~
|
|
||||||
"Password updated successfully"
|
|
||||||
|
|
||||||
assert Accounts.get_user_by_email_and_password(user.email, new_password)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "renders errors with invalid data (phx-change)", %{conn: conn} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
|
||||||
|
|
||||||
result =
|
|
||||||
lv
|
|
||||||
|> element("#password_form")
|
|
||||||
|> render_change(%{
|
|
||||||
"user" => %{
|
|
||||||
"password" => "too short",
|
|
||||||
"password_confirmation" => "does not match"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
assert result =~ "Save Password"
|
|
||||||
assert result =~ "should be at least 12 character(s)"
|
|
||||||
assert result =~ "does not match password"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "renders errors with invalid data (phx-submit)", %{conn: conn} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
|
||||||
|
|
||||||
result =
|
|
||||||
lv
|
|
||||||
|> form("#password_form", %{
|
|
||||||
"user" => %{
|
|
||||||
"password" => "too short",
|
|
||||||
"password_confirmation" => "does not match"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|> render_submit()
|
|
||||||
|
|
||||||
assert result =~ "Save Password"
|
|
||||||
assert result =~ "should be at least 12 character(s)"
|
|
||||||
assert result =~ "does not match password"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "confirm email" do
|
describe "confirm email" do
|
||||||
@ -176,33 +35,30 @@ defmodule SimpleshopThemeWeb.Auth.SettingsTest do
|
|||||||
test "updates the user email once", %{conn: conn, user: user, token: token, email: email} do
|
test "updates the user email once", %{conn: conn, user: user, token: token, email: email} do
|
||||||
{:error, redirect} = live(conn, ~p"/users/settings/confirm-email/#{token}")
|
{:error, redirect} = live(conn, ~p"/users/settings/confirm-email/#{token}")
|
||||||
|
|
||||||
assert {:live_redirect, %{to: path, flash: flash}} = redirect
|
assert {:redirect, %{to: "/admin/settings", flash: flash}} = redirect
|
||||||
assert path == ~p"/users/settings"
|
assert %{"info" => "Email changed successfully."} = flash
|
||||||
assert %{"info" => message} = flash
|
|
||||||
assert message == "Email changed successfully."
|
|
||||||
refute Accounts.get_user_by_email(user.email)
|
refute Accounts.get_user_by_email(user.email)
|
||||||
assert Accounts.get_user_by_email(email)
|
assert Accounts.get_user_by_email(email)
|
||||||
|
|
||||||
# use confirm token again
|
# use confirm token again
|
||||||
{:error, redirect} = live(conn, ~p"/users/settings/confirm-email/#{token}")
|
{:error, redirect} = live(conn, ~p"/users/settings/confirm-email/#{token}")
|
||||||
assert {:live_redirect, %{to: path, flash: flash}} = redirect
|
|
||||||
assert path == ~p"/users/settings"
|
assert {:redirect, %{to: "/admin/settings", flash: flash}} = redirect
|
||||||
assert %{"error" => message} = flash
|
assert %{"error" => "Email change link is invalid or it has expired."} = flash
|
||||||
assert message == "Email change link is invalid or it has expired."
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not update email with invalid token", %{conn: conn, user: user} do
|
test "does not update email with invalid token", %{conn: conn, user: user} do
|
||||||
{:error, redirect} = live(conn, ~p"/users/settings/confirm-email/oops")
|
{:error, redirect} = live(conn, ~p"/users/settings/confirm-email/oops")
|
||||||
assert {:live_redirect, %{to: path, flash: flash}} = redirect
|
|
||||||
assert path == ~p"/users/settings"
|
assert {:redirect, %{to: "/admin/settings", flash: flash}} = redirect
|
||||||
assert %{"error" => message} = flash
|
assert %{"error" => "Email change link is invalid or it has expired."} = flash
|
||||||
assert message == "Email change link is invalid or it has expired."
|
|
||||||
assert Accounts.get_user_by_email(user.email)
|
assert Accounts.get_user_by_email(user.email)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "redirects if user is not logged in", %{token: token} do
|
test "redirects if user is not logged in", %{token: token} do
|
||||||
conn = build_conn()
|
conn = build_conn()
|
||||||
{:error, redirect} = live(conn, ~p"/users/settings/confirm-email/#{token}")
|
{:error, redirect} = live(conn, ~p"/users/settings/confirm-email/#{token}")
|
||||||
|
|
||||||
assert {:redirect, %{to: path, flash: flash}} = redirect
|
assert {:redirect, %{to: path, flash: flash}} = redirect
|
||||||
assert path == ~p"/users/log-in"
|
assert path == ~p"/users/log-in"
|
||||||
assert %{"error" => message} = flash
|
assert %{"error" => message} = flash
|
||||||
|
|||||||
@ -80,7 +80,7 @@ defmodule SimpleshopThemeWeb.UserAuthTest do
|
|||||||
|> assign(:current_scope, Scope.for_user(user))
|
|> assign(:current_scope, Scope.for_user(user))
|
||||||
|> UserAuth.log_in_user(user)
|
|> UserAuth.log_in_user(user)
|
||||||
|
|
||||||
assert redirected_to(conn) == ~p"/users/settings"
|
assert redirected_to(conn) == ~p"/admin/settings"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "writes a cookie if remember_me was set in previous session", %{conn: conn, user: user} do
|
test "writes a cookie if remember_me was set in previous session", %{conn: conn, user: user} do
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user