From 2359cb920c1b56282431132a6cc7367771d44dc3 Mon Sep 17 00:00:00 2001 From: hq1 Date: Tue, 8 Oct 2024 10:30:01 +0200 Subject: [PATCH] Account settings w sidebar (#4654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Outline /settings/v2 fundamentals * Add setting tiles stubs * Bootstrap name change * Bootstrap theme change * Bootstrap security settings * Use table component for listing sessions * Disable current e-mail field * Implement Danger Zone * Deal with compilation warnings * Implement "Subscription" section * Implement invoices list * Fix invoices empty state & add API keys * Fix headings in Subscription section * Fix API keys mobile view * Fix subscription boxes width * Fix formatting * Move tests for settings WIP * Adjust remaining tests and router placement Include docs links in tiles, where applicable. * Fix remaining routes and remove dead code * Fix route in a live view where no @conn is available * Update mobile view settings picker * Format * Fix subscription section headings * Fix account e-mail on dark mode * Delete unused template * Fix mobile setting section picker * Optimize Login Management tile for mobile * Update invoices section with docs link * Update copy * Remove trailing dots from (sub)titles * Fix CSV export padding for "exporting" state * Align subscription status to the right * Fix failing test * Fix subscription status alignment once again * Improve subscription mobile view a little * Fixup test compilation 🙈 * Add extra margin to subscription status box * Make cancel button in 2FA modals expand in mobile view * Stats API only * Capitalize "Current session" indicator * Show "Show More" invoices button only when there's >12 * tiny change * Update changelog --------- Co-authored-by: Adrian Gruntkowski Co-authored-by: Marko Saric <34340819+metmarkosaric@users.noreply.github.com> --- CHANGELOG.md | 3 + lib/plausible/auth/user.ex | 12 + lib/plausible/auth/user_admin.ex | 3 +- lib/plausible_web.ex | 1 + .../components/billing/billing.ex | 6 +- lib/plausible_web/components/generic.ex | 5 +- lib/plausible_web/components/two_factor.ex | 10 +- .../controllers/auth_controller.ex | 223 +--- .../controllers/billing_controller.ex | 6 +- .../controllers/settings_controller.ex | 281 ++++ lib/plausible_web/live/components/form.ex | 10 + lib/plausible_web/live/csv_export.ex | 4 +- lib/plausible_web/live/sites.ex | 4 +- lib/plausible_web/plugs/require_account.ex | 2 +- lib/plausible_web/router.ex | 36 +- .../templates/auth/activate.html.heex | 2 +- .../generate_2fa_recovery_codes.html.heex | 2 +- .../auth/initiate_2fa_setup.html.heex | 2 +- .../templates/auth/new_api_key.html.eex | 24 - .../templates/auth/user_settings.html.heex | 650 ---------- .../templates/auth/verify_2fa_setup.html.heex | 2 +- .../billing/upgrade_success.html.heex | 2 +- .../templates/email/dashboard_locked.html.eex | 2 +- .../templates/email/over_limit.html.eex | 2 +- .../templates/layout/_header.html.heex | 6 +- .../templates/layout/_settings_tab.html.heex | 2 +- .../layout/_site_settings_tab.html.heex | 22 + .../templates/layout/settings.html.heex | 57 + .../templates/layout/site_settings.html.heex | 8 +- .../templates/settings/api_keys.html.heex | 56 + .../templates/settings/danger_zone.html.heex | 25 + .../templates/settings/invoices.html.heex | 55 + .../{auth => settings}/new_api_key.html.heex | 2 +- .../templates/settings/preferences.html.heex | 49 + .../templates/settings/security.html.heex | 246 ++++ .../templates/settings/subscription.html.heex | 104 ++ .../templates/stats/site_locked.html.heex | 2 +- lib/plausible_web/views/auth_view.ex | 28 +- lib/plausible_web/views/settings_view.ex | 36 + .../controllers/auth_controller_test.exs | 1099 +--------------- .../controllers/billing_controller_test.exs | 4 +- ....exs => settings_controller_sync_test.exs} | 6 +- .../controllers/settings_controller_test.exs | 1135 +++++++++++++++++ test/plausible_web/email_test.exs | 4 +- 44 files changed, 2188 insertions(+), 2052 deletions(-) create mode 100644 lib/plausible_web/controllers/settings_controller.ex delete mode 100644 lib/plausible_web/templates/auth/new_api_key.html.eex delete mode 100644 lib/plausible_web/templates/auth/user_settings.html.heex create mode 100644 lib/plausible_web/templates/layout/_site_settings_tab.html.heex create mode 100644 lib/plausible_web/templates/layout/settings.html.heex create mode 100644 lib/plausible_web/templates/settings/api_keys.html.heex create mode 100644 lib/plausible_web/templates/settings/danger_zone.html.heex create mode 100644 lib/plausible_web/templates/settings/invoices.html.heex rename lib/plausible_web/templates/{auth => settings}/new_api_key.html.heex (96%) create mode 100644 lib/plausible_web/templates/settings/preferences.html.heex create mode 100644 lib/plausible_web/templates/settings/security.html.heex create mode 100644 lib/plausible_web/templates/settings/subscription.html.heex create mode 100644 lib/plausible_web/views/settings_view.ex rename test/plausible_web/controllers/{auth_controller_sync_test.exs => settings_controller_sync_test.exs} (82%) create mode 100644 test/plausible_web/controllers/settings_controller_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f69eeaf4..e63373931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,10 @@ All notable changes to this project will be documented in this file. ### Changed +- Revised User Settings UI + ### Fixed + - Revenue metrics are displayed correctly after goal has been renamed ## v2.1.3 - 2024-09-26 diff --git a/lib/plausible/auth/user.ex b/lib/plausible/auth/user.ex index 341a4045a..116c1437f 100644 --- a/lib/plausible/auth/user.ex +++ b/lib/plausible/auth/user.ex @@ -75,6 +75,18 @@ defmodule Plausible.Auth.User do |> unique_constraint(:email) end + def name_changeset(user, attrs \\ %{}) do + user + |> cast(attrs, [:name]) + |> validate_required([:name]) + end + + def theme_changeset(user, attrs \\ %{}) do + user + |> cast(attrs, [:theme]) + |> validate_required([:theme]) + end + def settings_changeset(user, attrs \\ %{}) do user |> cast(attrs, [:email, :name, :theme]) diff --git a/lib/plausible/auth/user_admin.ex b/lib/plausible/auth/user_admin.ex index baf107d2a..4ebafa13b 100644 --- a/lib/plausible/auth/user_admin.ex +++ b/lib/plausible/auth/user_admin.ex @@ -119,7 +119,8 @@ defmodule Plausible.Auth.UserAdmin do defp subscription_status(user) do cond do user.subscription -> - status_str = PlausibleWeb.AuthView.present_subscription_status(user.subscription.status) + status_str = + PlausibleWeb.SettingsView.present_subscription_status(user.subscription.status) if user.subscription.paddle_subscription_id do {:safe, ~s(#{status_str})} diff --git a/lib/plausible_web.ex b/lib/plausible_web.ex index 3eefaad83..9044b258a 100644 --- a/lib/plausible_web.ex +++ b/lib/plausible_web.ex @@ -44,6 +44,7 @@ defmodule PlausibleWeb do import PlausibleWeb.ErrorHelpers import PlausibleWeb.FormHelpers import PlausibleWeb.Components.Generic + import PlausibleWeb.Live.Components.Form alias PlausibleWeb.Router.Helpers, as: Routes end end diff --git a/lib/plausible_web/components/billing/billing.ex b/lib/plausible_web/components/billing/billing.ex index 65bd1e29e..5adeef3de 100644 --- a/lib/plausible_web/components/billing/billing.ex +++ b/lib/plausible_web/components/billing/billing.ex @@ -17,8 +17,8 @@ defmodule PlausibleWeb.Components.Billing do def render_monthly_pageview_usage(assigns) do ~H"""
-

Monthly pageviews usage

-
+ <.title>Monthly pageviews usage +
    <.billing_cycle_tab name="Upcoming cycle" @@ -182,7 +182,7 @@ defmodule PlausibleWeb.Components.Billing do ~H"""

    Monthly quota

    diff --git a/lib/plausible_web/components/generic.ex b/lib/plausible_web/components/generic.ex index eb599b36e..cf741eadf 100644 --- a/lib/plausible_web/components/generic.ex +++ b/lib/plausible_web/components/generic.ex @@ -520,8 +520,10 @@ defmodule PlausibleWeb.Components.Generic do attr :rest, :global attr :width, :string, default: "min-w-full" attr :rows, :list, default: [] + attr :row_attrs, :any, default: nil slot :thead, required: false slot :tbody, required: true + slot :inner_block, required: false def table(assigns) do ~H""" @@ -532,9 +534,10 @@ defmodule PlausibleWeb.Components.Generic do - + <%= render_slot(@tbody, item) %> + <%= render_slot(@inner_block) %> """ diff --git a/lib/plausible_web/components/two_factor.ex b/lib/plausible_web/components/two_factor.ex index 148921947..66965ef3f 100644 --- a/lib/plausible_web/components/two_factor.ex +++ b/lib/plausible_web/components/two_factor.ex @@ -46,9 +46,13 @@ defmodule PlausibleWeb.Components.TwoFactor do autocomplete: "off", class: @input_class, oninput: - "this.value=this.value.replace(/[^0-9]/g, ''); if (this.value.length >= 6) document.getElementById('verify-button').focus()", + if @show_button? do + "this.value=this.value.replace(/[^0-9]/g, ''); if (this.value.length >= 6) document.getElementById('#{@id}').focus()" + else + "this.value=this.value.replace(/[^0-9]/g, '');" + end, onclick: "this.select();", - oninvalid: "document.getElementById('verify-button').disabled = false", + oninvalid: @show_button? && "document.getElementById('#{@id}').disabled = false", maxlength: "6", placeholder: "••••••", value: "", @@ -156,7 +160,7 @@ defmodule PlausibleWeb.Components.TwoFactor do <.button type="button" x-on:click={"#{@state_param} = false"} - class="mr-2" + class="w-full sm:w-auto mr-2" theme="bright" > Cancel diff --git a/lib/plausible_web/controllers/auth_controller.ex b/lib/plausible_web/controllers/auth_controller.ex index a1e371774..c326a8434 100644 --- a/lib/plausible_web/controllers/auth_controller.ex +++ b/lib/plausible_web/controllers/auth_controller.ex @@ -3,7 +3,6 @@ defmodule PlausibleWeb.AuthController do use Plausible.Repo alias Plausible.Auth - alias Plausible.Billing.Quota alias PlausibleWeb.TwoFactor alias PlausibleWeb.UserAuth @@ -26,15 +25,6 @@ defmodule PlausibleWeb.AuthController do plug( PlausibleWeb.RequireAccountPlug when action in [ - :user_settings, - :save_settings, - :update_email, - :update_password, - :cancel_update_email, - :new_api_key, - :create_api_key, - :delete_api_key, - :delete_session, :delete_me, :activate_form, :activate, @@ -261,10 +251,6 @@ defmodule PlausibleWeb.AuthController do end end - def user_settings(conn, _params) do - render_settings(conn, []) - end - def initiate_2fa_setup(conn, _params) do case Auth.TOTP.initiate(conn.assigns.current_user) do {:ok, user, %{totp_uri: totp_uri, secret: secret}} -> @@ -273,7 +259,7 @@ defmodule PlausibleWeb.AuthController do {:error, :already_setup} -> conn |> put_flash(:error, "Two-Factor Authentication is already setup for this account.") - |> redirect(to: Routes.auth_path(conn, :user_settings) <> "#setup-2fa") + |> redirect(to: Routes.settings_path(conn, :security) <> "#update-2fa") end end @@ -281,7 +267,7 @@ defmodule PlausibleWeb.AuthController do if Auth.TOTP.initiated?(conn.assigns.current_user) do render(conn, "verify_2fa_setup.html") else - redirect(conn, to: Routes.auth_path(conn, :user_settings) <> "#setup-2fa") + redirect(conn, to: Routes.settings_path(conn, :security) <> "#update-2fa") end end @@ -300,7 +286,7 @@ defmodule PlausibleWeb.AuthController do {:error, :not_initiated} -> conn |> put_flash(:error, "Please enable Two-Factor Authentication for this account first.") - |> redirect(to: Routes.auth_path(conn, :user_settings) <> "#setup-2fa") + |> redirect(to: Routes.settings_path(conn, :security) <> "#update-2fa") end end @@ -310,12 +296,12 @@ defmodule PlausibleWeb.AuthController do conn |> TwoFactor.Session.clear_remember_2fa() |> put_flash(:success, "Two-Factor Authentication is disabled") - |> redirect(to: Routes.auth_path(conn, :user_settings) <> "#setup-2fa") + |> redirect(to: Routes.settings_path(conn, :security) <> "#update-2fa") {:error, :invalid_password} -> conn |> put_flash(:error, "Incorrect password provided") - |> redirect(to: Routes.auth_path(conn, :user_settings) <> "#setup-2fa") + |> redirect(to: Routes.settings_path(conn, :security) <> "#update-2fa") end end @@ -329,12 +315,12 @@ defmodule PlausibleWeb.AuthController do {:error, :invalid_password} -> conn |> put_flash(:error, "Incorrect password provided") - |> redirect(to: Routes.auth_path(conn, :user_settings) <> "#setup-2fa") + |> redirect(to: Routes.settings_path(conn, :security) <> "#update-2fa") {:error, :not_enabled} -> conn |> put_flash(:error, "Please enable Two-Factor Authentication for this account first.") - |> redirect(to: Routes.auth_path(conn, :user_settings) <> "#setup-2fa") + |> redirect(to: Routes.settings_path(conn, :security) <> "#update-2fa") end end @@ -436,193 +422,10 @@ defmodule PlausibleWeb.AuthController do end end - def save_settings(conn, %{"user" => user_params}) do - user = conn.assigns.current_user - changes = Auth.User.settings_changeset(user, user_params) - - case Repo.update(changes) do - {:ok, _user} -> - conn - |> put_flash(:success, "Account settings saved successfully") - |> redirect(to: Routes.auth_path(conn, :user_settings)) - - {:error, changeset} -> - render_settings(conn, settings_changeset: changeset) - end - end - - def update_email(conn, %{"user" => user_params}) do - user = conn.assigns.current_user - - with :ok <- Auth.rate_limit(:email_change_user, user), - changes = Auth.User.email_changeset(user, user_params), - {:ok, user} <- Repo.update(changes) do - if user.email_verified do - handle_email_updated(conn) - else - Auth.EmailVerification.issue_code(user) - redirect(conn, to: Routes.auth_path(conn, :activate_form)) - end - else - {:error, %Ecto.Changeset{} = changeset} -> - render_settings(conn, email_changeset: changeset) - - {:error, {:rate_limit, _}} -> - changeset = - user - |> Auth.User.email_changeset(user_params) - |> Ecto.Changeset.add_error(:email, "too many requests, try again in an hour") - |> Map.put(:action, :validate) - - render_settings(conn, email_changeset: changeset) - end - end - - def update_password(conn, %{"user" => params}) do - user = conn.assigns.current_user - user_session = conn.assigns.current_user_session - - with :ok <- Auth.rate_limit(:password_change_user, user), - {:ok, user} <- do_update_password(user, params) do - UserAuth.revoke_all_user_sessions(user, except: user_session) - - conn - |> put_flash(:success, "Your password is now changed") - |> redirect(to: Routes.auth_path(conn, :user_settings) <> "#change-password") - else - {:error, %Ecto.Changeset{} = changeset} -> - render_settings(conn, password_changeset: changeset) - - {:error, {:rate_limit, _}} -> - changeset = - user - |> Auth.User.password_changeset(params) - |> Ecto.Changeset.add_error(:password, "too many attempts, try again in 20 minutes") - |> Map.put(:action, :validate) - - render_settings(conn, password_changeset: changeset) - end - end - - def cancel_update_email(conn, _params) do - changeset = Auth.User.cancel_email_changeset(conn.assigns.current_user) - - case Repo.update(changeset) do - {:ok, user} -> - conn - |> put_flash(:success, "Email changed back to #{user.email}") - |> redirect(to: Routes.auth_path(conn, :user_settings) <> "#change-email-address") - - {:error, _} -> - conn - |> put_flash( - :error, - "Could not cancel email update because previous email has already been taken" - ) - |> redirect(to: Routes.auth_path(conn, :activate_form)) - end - end - defp handle_email_updated(conn) do conn |> put_flash(:success, "Email updated successfully") - |> redirect(to: Routes.auth_path(conn, :user_settings) <> "#change-email-address") - end - - defp do_update_password(user, params) do - changes = Auth.User.password_changeset(user, params) - - Repo.transaction(fn -> - with {:ok, user} <- Repo.update(changes), - {:ok, user} <- validate_2fa_code(user, params["two_factor_code"]) do - user - else - {:error, :invalid_2fa} -> - changes - |> Ecto.Changeset.add_error(:password, "invalid 2FA code") - |> Map.put(:action, :validate) - |> Repo.rollback() - - {:error, changeset} -> - Repo.rollback(changeset) - end - end) - end - - defp validate_2fa_code(user, code) do - if Auth.TOTP.enabled?(user) do - case Auth.TOTP.validate_code(user, code) do - {:ok, user} -> {:ok, user} - {:error, :not_enabled} -> {:ok, user} - {:error, _} -> {:error, :invalid_2fa} - end - else - {:ok, user} - end - end - - defp render_settings(conn, opts) do - current_user = conn.assigns.current_user - api_keys = Repo.preload(current_user, :api_keys).api_keys - user_sessions = Auth.UserSessions.list_for_user(current_user) - - settings_changeset = - Keyword.get(opts, :settings_changeset, Auth.User.settings_changeset(current_user)) - - email_changeset = Keyword.get(opts, :email_changeset, Auth.User.email_changeset(current_user)) - - password_changeset = - Keyword.get(opts, :password_changeset, Auth.User.password_changeset(current_user)) - - render(conn, "user_settings.html", - api_keys: api_keys, - user_sessions: user_sessions, - settings_changeset: settings_changeset, - email_changeset: email_changeset, - password_changeset: password_changeset, - subscription: current_user.subscription, - invoices: Plausible.Billing.paddle_api().get_invoices(current_user.subscription), - theme: current_user.theme || "system", - team_member_limit: Quota.Limits.team_member_limit(current_user), - team_member_usage: Quota.Usage.team_member_usage(current_user), - site_limit: Quota.Limits.site_limit(current_user), - site_usage: Quota.Usage.site_usage(current_user), - pageview_limit: Quota.Limits.monthly_pageview_limit(current_user), - pageview_usage: Quota.Usage.monthly_pageview_usage(current_user), - totp_enabled?: Auth.TOTP.enabled?(current_user) - ) - end - - def new_api_key(conn, _params) do - changeset = Auth.ApiKey.changeset(%Auth.ApiKey{}) - - render(conn, "new_api_key.html", changeset: changeset) - end - - def create_api_key(conn, %{"api_key" => %{"name" => name, "key" => key}}) do - case Auth.create_api_key(conn.assigns.current_user, name, key) do - {:ok, _api_key} -> - conn - |> put_flash(:success, "API key created successfully") - |> redirect(to: "/settings#api-keys") - - {:error, changeset} -> - render(conn, "new_api_key.html", changeset: changeset) - end - end - - def delete_api_key(conn, %{"id" => id}) do - case Auth.delete_api_key(conn.assigns.current_user, id) do - :ok -> - conn - |> put_flash(:success, "API key revoked successfully") - |> redirect(to: "/settings#api-keys") - - {:error, :not_found} -> - conn - |> put_flash(:error, "Could not find API Key to delete") - |> redirect(to: "/settings#api-keys") - end + |> redirect(to: Routes.settings_path(conn, :security) <> "#update-email") end def delete_me(conn, params) do @@ -631,16 +434,6 @@ defmodule PlausibleWeb.AuthController do logout(conn, params) end - def delete_session(conn, %{"id" => session_id}) do - current_user = conn.assigns.current_user - - :ok = UserAuth.revoke_user_session(current_user, session_id) - - conn - |> put_flash(:success, "Session logged out successfully") - |> redirect(to: "/settings#user-sessions") - end - def logout(conn, params) do redirect_to = Map.get(params, "redirect", "/") diff --git a/lib/plausible_web/controllers/billing_controller.ex b/lib/plausible_web/controllers/billing_controller.ex index 57d121297..cdc2c708f 100644 --- a/lib/plausible_web/controllers/billing_controller.ex +++ b/lib/plausible_web/controllers/billing_controller.ex @@ -44,7 +44,7 @@ defmodule PlausibleWeb.BillingController do Subscription.Status.past_due(), Subscription.Status.paused() ]) -> - redirect(conn, to: Routes.auth_path(conn, :user_settings)) + redirect(conn, to: Routes.settings_path(conn, :subscription)) subscribed_to_latest? -> render(conn, "change_enterprise_plan_contact_us.html", skip_plausible_tracking: true) @@ -101,7 +101,7 @@ defmodule PlausibleWeb.BillingController do {:ok, _subscription} -> conn |> put_flash(:success, "Plan changed successfully") - |> redirect(to: "/settings") + |> redirect(to: Routes.settings_path(conn, :subscription)) {:error, e} -> msg = @@ -131,7 +131,7 @@ defmodule PlausibleWeb.BillingController do conn |> put_flash(:error, msg) - |> redirect(to: "/settings") + |> redirect(to: Routes.settings_path(conn, :subscription)) end end diff --git a/lib/plausible_web/controllers/settings_controller.ex b/lib/plausible_web/controllers/settings_controller.ex new file mode 100644 index 000000000..0109e5597 --- /dev/null +++ b/lib/plausible_web/controllers/settings_controller.ex @@ -0,0 +1,281 @@ +defmodule PlausibleWeb.SettingsController do + use PlausibleWeb, :controller + use Plausible.Repo + + alias Plausible.Auth + alias PlausibleWeb.UserAuth + + alias Plausible.Billing.Quota + + require Logger + + def index(conn, _params) do + redirect(conn, to: Routes.settings_path(conn, :preferences)) + end + + def preferences(conn, _params) do + render_preferences(conn) + end + + def security(conn, _params) do + render_security(conn) + end + + def subscription(conn, _params) do + current_user = conn.assigns.current_user + + render(conn, :subscription, + layout: {PlausibleWeb.LayoutView, :settings}, + subscription: current_user.subscription, + pageview_limit: Quota.Limits.monthly_pageview_limit(current_user), + pageview_usage: Quota.Usage.monthly_pageview_usage(current_user), + site_usage: Quota.Usage.site_usage(current_user), + site_limit: Quota.Limits.site_limit(current_user), + team_member_limit: Quota.Limits.team_member_limit(current_user), + team_member_usage: Quota.Usage.team_member_usage(current_user) + ) + end + + def invoices(conn, _params) do + current_user = conn.assigns.current_user + invoices = Plausible.Billing.paddle_api().get_invoices(current_user.subscription) + render(conn, :invoices, layout: {PlausibleWeb.LayoutView, :settings}, invoices: invoices) + end + + def api_keys(conn, _params) do + current_user = conn.assigns.current_user + + api_keys = + Repo.preload(current_user, :api_keys).api_keys + + render(conn, :api_keys, layout: {PlausibleWeb.LayoutView, :settings}, api_keys: api_keys) + end + + def new_api_key(conn, _params) do + changeset = Auth.ApiKey.changeset(%Auth.ApiKey{}) + + render(conn, "new_api_key.html", changeset: changeset) + end + + def create_api_key(conn, %{"api_key" => %{"name" => name, "key" => key}}) do + case Auth.create_api_key(conn.assigns.current_user, name, key) do + {:ok, _api_key} -> + conn + |> put_flash(:success, "API key created successfully") + |> redirect(to: Routes.settings_path(conn, :api_keys) <> "#api-keys") + + {:error, changeset} -> + render(conn, "new_api_key.html", changeset: changeset) + end + end + + def delete_api_key(conn, %{"id" => id}) do + case Auth.delete_api_key(conn.assigns.current_user, id) do + :ok -> + conn + |> put_flash(:success, "API key revoked successfully") + |> redirect(to: Routes.settings_path(conn, :api_keys) <> "#api-keys") + + {:error, :not_found} -> + conn + |> put_flash(:error, "Could not find API Key to delete") + |> redirect(to: Routes.settings_path(conn, :api_keys) <> "#api-keys") + end + end + + def danger_zone(conn, _params) do + render(conn, :danger_zone, layout: {PlausibleWeb.LayoutView, :settings}) + end + + # Preferences actions + + def update_name(conn, %{"user" => params}) do + changeset = Auth.User.name_changeset(conn.assigns.current_user, params) + + case Repo.update(changeset) do + {:ok, _user} -> + conn + |> put_flash(:success, "Name changed") + |> redirect(to: Routes.settings_path(conn, :preferences) <> "#update-name") + + {:error, changeset} -> + render_preferences(conn, name_changeset: changeset) + end + end + + def update_theme(conn, %{"user" => params}) do + changeset = Auth.User.theme_changeset(conn.assigns.current_user, params) + + case Repo.update(changeset) do + {:ok, _user} -> + conn + |> put_flash(:success, "Theme changed") + |> redirect(to: Routes.settings_path(conn, :preferences) <> "#update-theme") + + {:error, changeset} -> + render_preferences(conn, theme_changeset: changeset) + end + end + + defp render_preferences(conn, opts \\ []) do + name_changeset = + Keyword.get(opts, :name_changeset, Auth.User.name_changeset(conn.assigns.current_user)) + + theme_changeset = + Keyword.get(opts, :theme_changeset, Auth.User.theme_changeset(conn.assigns.current_user)) + + render(conn, :preferences, + name_changeset: name_changeset, + theme_changeset: theme_changeset, + layout: {PlausibleWeb.LayoutView, :settings} + ) + end + + # Security actions + + def update_email(conn, %{"user" => params}) do + user = conn.assigns.current_user + + with :ok <- Auth.rate_limit(:email_change_user, user), + changes = Auth.User.email_changeset(user, params), + {:ok, user} <- Repo.update(changes) do + if user.email_verified do + handle_email_updated(conn) + else + Auth.EmailVerification.issue_code(user) + redirect(conn, to: Routes.auth_path(conn, :activate_form)) + end + else + {:error, %Ecto.Changeset{} = changeset} -> + render_security(conn, email_changeset: changeset) + + {:error, {:rate_limit, _}} -> + changeset = + user + |> Auth.User.email_changeset(params) + |> Ecto.Changeset.add_error(:email, "too many requests, try again in an hour") + |> Map.put(:action, :validate) + + render_security(conn, email_changeset: changeset) + end + end + + def cancel_update_email(conn, _params) do + changeset = Auth.User.cancel_email_changeset(conn.assigns.current_user) + + case Repo.update(changeset) do + {:ok, user} -> + conn + |> put_flash(:success, "Email changed back to #{user.email}") + |> redirect(to: Routes.settings_path(conn, :security) <> "#update-email") + + {:error, _} -> + conn + |> put_flash( + :error, + "Could not cancel email update because previous email has already been taken" + ) + |> redirect(to: Routes.auth_path(conn, :activate_form)) + end + end + + def update_password(conn, %{"user" => params}) do + user = conn.assigns.current_user + user_session = conn.assigns.current_user_session + + with :ok <- Auth.rate_limit(:password_change_user, user), + {:ok, user} <- do_update_password(user, params) do + UserAuth.revoke_all_user_sessions(user, except: user_session) + + conn + |> put_flash(:success, "Your password is now changed") + |> redirect(to: Routes.settings_path(conn, :security) <> "#update-password") + else + {:error, %Ecto.Changeset{} = changeset} -> + render_security(conn, password_changeset: changeset) + + {:error, {:rate_limit, _}} -> + changeset = + user + |> Auth.User.password_changeset(params) + |> Ecto.Changeset.add_error(:password, "too many attempts, try again in 20 minutes") + |> Map.put(:action, :validate) + + render_security(conn, password_changeset: changeset) + end + end + + defp render_security(conn, opts \\ []) do + user_sessions = Auth.UserSessions.list_for_user(conn.assigns.current_user) + + email_changeset = + Keyword.get( + opts, + :email_changeset, + Auth.User.email_changeset(conn.assigns.current_user, %{email: ""}) + ) + + password_changeset = + Keyword.get( + opts, + :password_changeset, + Auth.User.password_changeset(conn.assigns.current_user) + ) + + render(conn, :security, + totp_enabled?: Auth.TOTP.enabled?(conn.assigns.current_user), + user_sessions: user_sessions, + email_changeset: email_changeset, + password_changeset: password_changeset, + layout: {PlausibleWeb.LayoutView, :settings} + ) + end + + def delete_session(conn, %{"id" => session_id}) do + current_user = conn.assigns.current_user + + :ok = UserAuth.revoke_user_session(current_user, session_id) + + conn + |> put_flash(:success, "Session logged out successfully") + |> redirect(to: Routes.settings_path(conn, :security) <> "#user-sessions") + end + + defp do_update_password(user, params) do + changes = Auth.User.password_changeset(user, params) + + Repo.transaction(fn -> + with {:ok, user} <- Repo.update(changes), + {:ok, user} <- validate_2fa_code(user, params["two_factor_code"]) do + user + else + {:error, :invalid_2fa} -> + changes + |> Ecto.Changeset.add_error(:password, "invalid 2FA code") + |> Map.put(:action, :validate) + |> Repo.rollback() + + {:error, changeset} -> + Repo.rollback(changeset) + end + end) + end + + defp validate_2fa_code(user, code) do + if Auth.TOTP.enabled?(user) do + case Auth.TOTP.validate_code(user, code) do + {:ok, user} -> {:ok, user} + {:error, :not_enabled} -> {:ok, user} + {:error, _} -> {:error, :invalid_2fa} + end + else + {:ok, user} + end + end + + defp handle_email_updated(conn) do + conn + |> put_flash(:success, "Email updated") + |> redirect(to: Routes.settings_path(conn, :security) <> "#update-email") + end +end diff --git a/lib/plausible_web/live/components/form.ex b/lib/plausible_web/live/components/form.ex index efd1d284b..7a1e3ddce 100644 --- a/lib/plausible_web/live/components/form.ex +++ b/lib/plausible_web/live/components/form.ex @@ -50,6 +50,7 @@ defmodule PlausibleWeb.Live.Components.Form do attr(:class, :any, default: @default_input_class) attr(:mt?, :boolean, default: true) + attr(:max_one_error, :boolean, default: false) slot(:inner_block) def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do @@ -82,6 +83,15 @@ defmodule PlausibleWeb.Live.Components.Form do # All other inputs text, datetime-local, url, password, etc. are handled here... def input(assigns) do + errors = + if assigns.max_one_error do + Enum.take(assigns.errors, 1) + else + assigns.errors + end + + assigns = assign(assigns, :errors, errors) + ~H"""
    <.label :if={@label != nil and @label != ""} for={@id} class="mb-2"> diff --git a/lib/plausible_web/live/csv_export.ex b/lib/plausible_web/live/csv_export.ex index 62df94ca2..cf3de1921 100644 --- a/lib/plausible_web/live/csv_export.ex +++ b/lib/plausible_web/live/csv_export.ex @@ -132,13 +132,13 @@ defmodule PlausibleWeb.Live.CSVExport do
    -

    +

    The preparation of your stats might take a while. Depending on the volume of your data, it might take up to 20 minutes. Feel free to leave the page and return later.

    """ diff --git a/lib/plausible_web/live/sites.ex b/lib/plausible_web/live/sites.ex index 27704d44a..4fb3e96d5 100644 --- a/lib/plausible_web/live/sites.ex +++ b/lib/plausible_web/live/sites.ex @@ -143,7 +143,7 @@ defmodule PlausibleWeb.Live.Sites do

    To access the sites you own, you need to subscribe to a monthly or yearly payment plan. <%= link( "Upgrade now →", - to: "/settings", + to: Routes.settings_path(PlausibleWeb.Endpoint, :subscription), class: "text-sm font-medium text-yellow-800" ) %>

    @@ -450,7 +450,7 @@ defmodule PlausibleWeb.Live.Sites do You can review your usage in the <.styled_link class="inline-block" - href={Routes.auth_path(PlausibleWeb.Endpoint, :user_settings)} + href={Routes.settings_path(PlausibleWeb.Endpoint, :subscription)} > account settings . diff --git a/lib/plausible_web/plugs/require_account.ex b/lib/plausible_web/plugs/require_account.ex index 4073cd79a..7d6f4c98a 100644 --- a/lib/plausible_web/plugs/require_account.ex +++ b/lib/plausible_web/plugs/require_account.ex @@ -2,7 +2,7 @@ defmodule PlausibleWeb.RequireAccountPlug do import Plug.Conn @unverified_email_exceptions [ - ["settings", "email", "cancel"], + ["settings", "security", "email", "cancel"], ["activate"], ["activate", "request-code"], ["me"] diff --git a/lib/plausible_web/router.ex b/lib/plausible_web/router.ex index db5f828eb..7ee5347f6 100644 --- a/lib/plausible_web/router.ex +++ b/lib/plausible_web/router.ex @@ -322,20 +322,38 @@ defmodule PlausibleWeb.Router do post "/share/:slug/authenticate", StatsController, :authenticate_shared_link end + scope "/settings", PlausibleWeb do + pipe_through [:browser, :csrf, PlausibleWeb.RequireAccountPlug] + + get "/", SettingsController, :index + get "/preferences", SettingsController, :preferences + + post "/preferences/name", SettingsController, :update_name + post "/preferences/theme", SettingsController, :update_theme + + get "/security", SettingsController, :security + delete "/security/user-sessions/:id", SettingsController, :delete_session + + post "/security/email/cancel", SettingsController, :cancel_update_email + post "/security/email", SettingsController, :update_email + post "/security/password", SettingsController, :update_password + + get "/billing/subscription", SettingsController, :subscription + get "/billing/invoices", SettingsController, :invoices + get "/api-keys", SettingsController, :api_keys + + get "/api-keys/new", SettingsController, :new_api_key + post "/api-keys", SettingsController, :create_api_key + delete "/api-keys/:id", SettingsController, :delete_api_key + + get "/danger-zone", SettingsController, :danger_zone + end + scope "/", PlausibleWeb do pipe_through [:browser, :csrf] get "/logout", AuthController, :logout - get "/settings", AuthController, :user_settings - put "/settings", AuthController, :save_settings - put "/settings/email", AuthController, :update_email - put "/settings/password", AuthController, :update_password - post "/settings/email/cancel", AuthController, :cancel_update_email delete "/me", AuthController, :delete_me - get "/settings/api-keys/new", AuthController, :new_api_key - post "/settings/api-keys", AuthController, :create_api_key - delete "/settings/api-keys/:id", AuthController, :delete_api_key - delete "/settings/user-sessions/:id", AuthController, :delete_session get "/auth/google/callback", AuthController, :google_auth_callback diff --git a/lib/plausible_web/templates/auth/activate.html.heex b/lib/plausible_web/templates/auth/activate.html.heex index 57a1bab1a..0915003fb 100644 --- a/lib/plausible_web/templates/auth/activate.html.heex +++ b/lib/plausible_web/templates/auth/activate.html.heex @@ -88,7 +88,7 @@ <.focus_list> <:item :if={@has_any_memberships?}> - <.styled_link method="post" href="/settings/email/cancel"> + <.styled_link method="post" href={Routes.settings_path(@conn, :cancel_update_email)}> Change email back to <%= @conn.assigns[:current_user].previous_email %> diff --git a/lib/plausible_web/templates/auth/generate_2fa_recovery_codes.html.heex b/lib/plausible_web/templates/auth/generate_2fa_recovery_codes.html.heex index 7f130c7fe..01a8e4c5d 100644 --- a/lib/plausible_web/templates/auth/generate_2fa_recovery_codes.html.heex +++ b/lib/plausible_web/templates/auth/generate_2fa_recovery_codes.html.heex @@ -40,7 +40,7 @@ diff --git a/lib/plausible_web/templates/auth/initiate_2fa_setup.html.heex b/lib/plausible_web/templates/auth/initiate_2fa_setup.html.heex index a0a322b08..09c569a6b 100644 --- a/lib/plausible_web/templates/auth/initiate_2fa_setup.html.heex +++ b/lib/plausible_web/templates/auth/initiate_2fa_setup.html.heex @@ -11,7 +11,7 @@ <.focus_list> <:item> Changed your mind? - <.styled_link href={Routes.auth_path(@conn, :user_settings) <> "#setup-2fa"}> + <.styled_link href={Routes.settings_path(@conn, :security) <> "#update-2fa"}> Go back to Settings diff --git a/lib/plausible_web/templates/auth/new_api_key.html.eex b/lib/plausible_web/templates/auth/new_api_key.html.eex deleted file mode 100644 index f08cab28c..000000000 --- a/lib/plausible_web/templates/auth/new_api_key.html.eex +++ /dev/null @@ -1,24 +0,0 @@ - -<%= form_for @changeset, "/settings/api-keys", [class: "w-full max-w-md mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 py-6 mt-8"], fn f -> %> -

    Create new API key

    -
    - <%= label f, :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> -
    - <%= text_input f, :name, class: "dark:bg-gray-900 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 dark:text-gray-300 dark:focus:bg-gray-800 focus:border-gray-300 dark:focus:border-gray-500 rounded-md", placeholder: "Development" %> -
    - <%= error_tag f, :name %> -
    -
    - <%= label f, :key, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> -
    - <%= text_input f, :key, id: "key-input", class: "dark:text-gray-300 shadow-sm bg-gray-50 dark:bg-gray-850 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md pr-16", readonly: "readonly" %> - - COPY - - <%= error_tag f, :key %> -

    Make sure to store the key in a secure place. Once created, we will not be able to show it again.

    -
    -
    - <%= submit "Continue", class: "button mt-4 w-full" %> -<% end %> -
    diff --git a/lib/plausible_web/templates/auth/user_settings.html.heex b/lib/plausible_web/templates/auth/user_settings.html.heex deleted file mode 100644 index 80d4eb4ac..000000000 --- a/lib/plausible_web/templates/auth/user_settings.html.heex +++ /dev/null @@ -1,650 +0,0 @@ -
    - <%= if ee?() do %> -
    -
    -

    Subscription Plan

    -
    - - Business - - - <%= present_subscription_status(@subscription.status) %> - -
    -
    - -
    - - - -
    - -
    -

    Next bill amount

    - <%= if Plausible.Billing.Subscription.Status.in?(@subscription, [Plausible.Billing.Subscription.Status.active(), Plausible.Billing.Subscription.Status.past_due()]) do %> -
    - <%= PlausibleWeb.BillingView.present_currency(@subscription.currency_code) %><%= @subscription.next_bill_amount %> -
    - <.styled_link :if={@subscription.update_url} href={@subscription.update_url}> - Update billing info - - <% else %> -
    ---
    - <% end %> -
    -
    -

    Next bill date

    - - <%= if @subscription && @subscription.next_bill_date && Plausible.Billing.Subscription.Status.in?(@subscription, [Plausible.Billing.Subscription.Status.active(), Plausible.Billing.Subscription.Status.past_due()]) do %> -
    - <%= Calendar.strftime(@subscription.next_bill_date, "%b %-d, %Y") %> -
    - - (<%= subscription_interval(@subscription) %> billing) - - <% else %> -
    ---
    - <% end %> -
    -
    - - - -
    -

    Sites & team members usage

    - - - - -
    - - <%= cond do %> - <% Plausible.Billing.Subscriptions.resumable?(@subscription) && @subscription.cancel_url -> %> -
    - <%= link("Cancel my subscription", - to: @subscription.cancel_url, - class: - "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 bg-white dark:bg-gray-800 hover:text-red-500 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150" - ) %> -
    - <% true -> %> -
    - -
    - <% end %> -
    - - <%= case @invoices do %> - <% {:error, :no_invoices} -> %> - <% {:error, :request_failed} -> %> -
    -

    Invoices

    -
    -

    - Something went wrong -

    -
    - <% {:ok, invoice_list} when is_list(invoice_list) -> %> -
    -

    Invoices

    -
    - - - - - - - - - <%= for {invoice, idx} <- Enum.with_index(format_invoices(invoice_list)) do %> - - - - - - - 12} x-show="!showAll"> - - - - <% end %> -
    - Date - - Amount - - Invoice -
    - <%= invoice.date %> - - <%= invoice.currency <> invoice.amount %> - - <%= link("Link", to: invoice.url, target: "_blank") %> -
    - -
    -
    - <% end %> - <% end %> - -
    -

    Dashboard Appearance

    - -
    - - <%= form_for @settings_changeset, "/settings", [class: "max-w-sm"], fn f -> %> -
    - <%= label(f, :theme, "Theme Selection", - class: "block text-sm font-medium leading-5 text-gray-700 dark:text-gray-300" - ) %> - <%= select(f, :theme, Plausible.Themes.options(), - class: - "dark:bg-gray-900 mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:text-gray-100 cursor-pointer" - ) %> -
    - - - Save - - <% end %> -
    - -
    -

    - Change password -

    - -
    - - <%= form_for @password_changeset, "/settings/password#change-password", [class: "max-w-sm"], fn f -> %> -
    - <%= label(f, :old_password, "Current password", - class: "block text-sm font-medium text-gray-700 dark:text-gray-300" - ) %> -
    - <%= password_input(f, :old_password, - class: - "shadow-sm dark:bg-gray-900 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:bg-gray-800" - ) %> - <%= error_tag(f, :old_password, only_first?: true) %> -
    -
    -
    - <%= label(f, :password, "New password", - class: "block text-sm font-medium text-gray-700 dark:text-gray-300" - ) %> -
    - <%= password_input(f, :password, - autocomplete: "new-password", - class: - "shadow-sm dark:bg-gray-900 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:bg-gray-800" - ) %> - <%= error_tag(f, :password, only_first?: true) %> -
    -
    -
    - <%= label(f, :password_confirmation, "Confirm new password", - class: "block text-sm font-medium text-gray-700 dark:text-gray-300" - ) %> -
    - <%= password_input(f, :password_confirmation, - autocomplete: "new-password", - class: - "shadow-sm dark:bg-gray-900 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:bg-gray-800" - ) %> - <%= error_tag(f, :password_confirmation, only_first?: true) %> -
    -
    - -
    - <%= label(f, :two_factor_code, "Verify with 2FA", - class: "block text-sm font-medium text-gray-700 dark:text-gray-300" - ) %> -
    - -
    -
    - - <%= submit("Change my password", - class: - "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-600 dark:text-red-500 bg-white dark:bg-gray-800 hover:text-red-400 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150" - ) %> - <% end %> -
    - -
    -

    - Two-Factor Authentication (2FA) -

    - -

    - Two-Factor Authentication protects your account by adding an extra security step when you log in. -

    - - <%= if @totp_enabled? do %> - - -

    - Lost your recovery codes? - - Generate new - -

    - <% else %> - <%= form_for @conn.params, Routes.auth_path(@conn, :initiate_2fa_setup), fn _ -> %> - - Enable 2FA - - <% end %> - <% end %> -
    - -
    -

    - Change account name -

    - -
    - - <%= form_for @settings_changeset, "/settings#change-account-name", [class: "max-w-sm"], fn f -> %> -
    - <%= label(f, :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300") %> -
    - <%= text_input(f, :name, - class: - "shadow-sm dark:bg-gray-900 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:bg-gray-800" - ) %> - <%= error_tag(f, :name) %> -
    -
    - - Save - - <% end %> -
    - -
    -

    - Change email address -

    - -
    - - <%= form_for @email_changeset, "/settings/email#change-email-address", [class: "max-w-sm"], fn f -> %> -
    - <%= label(f, :password, "Account password", - class: "block text-sm font-medium text-gray-700 dark:text-gray-300" - ) %> -
    - <%= password_input(f, :password, - class: - "shadow-sm dark:bg-gray-900 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:bg-gray-800" - ) %> - <%= error_tag(f, :password) %> -
    -
    -
    - <%= label(f, :current_email, "Current email", - class: "block text-sm font-medium text-gray-700 dark:text-gray-300" - ) %> -
    - <%= email_input(f, :current_email, - readonly: true, - value: f.data.email, - class: - "shadow-sm dark:bg-gray-900 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:bg-gray-800 bg-gray-100" - ) %> -
    -
    -
    - <%= label(f, :email, "New email", - class: "block text-sm font-medium text-gray-700 dark:text-gray-300" - ) %> -
    - <%= email_input(f, :email, - value: f.params["email"], - class: - "shadow-sm dark:bg-gray-900 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md" - ) %> - <%= error_tag(f, :email) %> -
    -
    - - <%= submit("Change my email", - class: - "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-600 dark:text-red-500 bg-white dark:bg-gray-800 hover:text-red-400 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150" - ) %> - <% end %> -
    - -
    -

    Login Management

    - -
    - -

    - Log out of your account on other devices. Note that logged-in sessions automatically expire after 14 days of inactivity. -

    - -
    -
    -
    - <%= if Enum.any?(@user_sessions) do %> -
    - - - - - - - - - - <%= for session <- @user_sessions do %> - - - - - - <% end %> - -
    - Device - - Last seen - - Log Out -
    - <%= session.device %> - - <%= Plausible.Auth.UserSessions.last_used_humanize(session) %> - - <%= if @current_user_session.id == session.id do %> - - Current Session - - <% else %> - <%= button("Log Out", - to: "/settings/user-sessions/#{session.id}", - class: "text-red-600 hover:text-red-400 dark:text-red-500", - method: :delete, - "data-confirm": "Are you sure you want to log out this session?" - ) %> - <% end %> -
    -
    - <% end %> -
    -
    -
    -
    - -
    -

    API Keys

    -
    - - - -
    -
    -
    - <%= if Enum.any?(@api_keys) do %> -
    - - - - - - - - - - <%= for api_key <- @api_keys do %> - - - - - - <% end %> - -
    - Name - - Key - - Revoke -
    - <%= api_key.name %> - - <%= api_key.key_prefix %><%= String.duplicate("*", 32 - 6) %> - - <%= button("Revoke", - to: "/settings/api-keys/#{api_key.id}", - class: "text-red-600 hover:text-red-400 dark:text-red-500", - method: :delete, - "data-confirm": - "Are you sure you want to revoke this key? This action cannot be reversed." - ) %> -
    -
    - <% end %> - - - + New API Key - -
    -
    -
    -
    - -
    -
    -

    Delete account

    -
    - -
    - -

    - Deleting your account removes all sites and stats you've collected -

    - - <%= if Plausible.Billing.Subscription.Status.active?(@subscription) do %> - - Delete my account - -

    - Your account cannot be deleted because you have an active subscription. If you want to delete your account, please cancel your subscription first. -

    - <% else %> - <%= link("Delete my account", - to: "/me", - class: - "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 dark:text-red-500 bg-white dark:bg-gray-800 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150", - method: "delete", - data: [ - confirm: - "Deleting your account will also delete all the sites and data that you own. This action cannot be reversed. Are you sure?" - ] - ) %> - <% end %> -
    - - - <:icon> - - - <:buttons> - <.button type="submit" class="w-full sm:w-auto"> - Disable 2FA - - - -
    - Once disabled, verification codes from the authenticator application and current recovery codes will become invalid. 2FA will have to be setup from the start. -
    - -
    - Enter your password to disable 2FA. -
    - -
    - <%= password_input(f, :password, - id: "disable_2fa_password", - placeholder: "Enter password", - "x-ref": "disable2FAPassword", - class: - "shadow-sm dark:bg-gray-900 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:bg-gray-800" - ) %> -
    -
    - - - <:icon> - - - - <:buttons> - <.button - id="generate-2fa-recovery-button" - type="submit" - class="w-full sm:w-auto [&>span.label-enabled]:block [&>span.label-disabled]:hidden [&[disabled]>span.label-enabled]:hidden [&[disabled]>span.label-disabled]:block" - > - - Generate New Codes - - - - <.spinner class="inline-block h-5 w-5 mr-2 text-white dark:text-gray-400" /> - Generating Codes - - - - -
    - If you generate new codes, the old ones will become invalid. -
    - -
    - Enter your password to continue. -
    - -
    - <%= password_input(f, :password, - id: "regenerate_2fa_password", - placeholder: "Enter password", - "x-ref": "regenerate2FAPassword", - class: - "w-full shadow-sm dark:bg-gray-900 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:bg-gray-800" - ) %> -
    -
    -
    diff --git a/lib/plausible_web/templates/auth/verify_2fa_setup.html.heex b/lib/plausible_web/templates/auth/verify_2fa_setup.html.heex index 353e832c1..7392adee0 100644 --- a/lib/plausible_web/templates/auth/verify_2fa_setup.html.heex +++ b/lib/plausible_web/templates/auth/verify_2fa_setup.html.heex @@ -11,7 +11,7 @@ <.focus_list> <:item> Changed your mind? - <.styled_link href={Routes.auth_path(@conn, :user_settings) <> "#setup-2fa"}> + <.styled_link href={Routes.settings_path(@conn, :security) <> "#update-2fa"}> Go back to Settings diff --git a/lib/plausible_web/templates/billing/upgrade_success.html.heex b/lib/plausible_web/templates/billing/upgrade_success.html.heex index 35c5e8e69..788291147 100644 --- a/lib/plausible_web/templates/billing/upgrade_success.html.heex +++ b/lib/plausible_web/templates/billing/upgrade_success.html.heex @@ -20,7 +20,7 @@