~mlb/linkhut

9a7853847ea7b18929920d8fa7660c1886544abb — Matías Larre Borges 6 days ago fe7bbe3
Add self-service support for deleting own account
M assets/css/app.scss => assets/css/app.scss +2 -1
@@ 197,7 197,7 @@ fieldset {
    border-color: transparent;
    border-width: 0;
    border-style: solid;
    margin: 0;
    margin: 0 0 $spacing-small;
    padding: 0;

    & input, & textarea {


@@ 228,6 228,7 @@ fieldset {
    & input[type=checkbox] {
        margin-bottom: 0;
        margin-right: $spacing-small;
        margin-left: 0;
        vertical-align: middle;
    }


M lib/linkhut/accounts.ex => lib/linkhut/accounts.ex +16 -5
@@ 122,16 122,27 @@ defmodule Linkhut.Accounts do

  ## Examples

      iex> delete_user(user)
      iex> delete_user(user, %{"confirmed" => "true"})
      {:ok, %User{}}

      iex> delete_user(user)
      iex> delete_user(user, %{})
      {:error, %Ecto.Changeset{}}

  """
  @spec delete_user(User.t()) :: {:ok, User.t()} | {:error, changeset(User.t())}
  def delete_user(%User{} = user) do
    Repo.delete(user)
  @spec delete_user(changeset(User.t()), %{optional(any) => any}) ::
          {:ok, User.t()} | {:error, changeset(User.t())}
  def delete_user(%User{} = user, attrs) do
    user
    |> Repo.preload(:credential)
    |> User.changeset(attrs)
    |> Ecto.Changeset.validate_acceptance(:confirmed,
      message: "Please confirm you want to delete your account"
    )
    |> Ecto.Changeset.no_assoc_constraint(:applications,
      message:
        "You still own OAuth applications, you must delete those before deleting your account"
    )
    |> Repo.delete()
  end

  @doc """

M lib/linkhut/accounts/user.ex => lib/linkhut/accounts/user.ex +13 -1
@@ 22,7 22,19 @@ defmodule Linkhut.Accounts.User do
    field :roles, {:array, Ecto.Enum}, values: [:admin], default: []
    has_one :credential, Credential

    has_many :links, Link, references: :id
    has_many :links, Link, references: :id, on_delete: :delete_all

    has_many :applications, Linkhut.Oauth.Application, foreign_key: :owner_id, references: :id

    has_many :access_grants, Linkhut.Oauth.AccessGrant,
      foreign_key: :resource_owner_id,
      references: :id,
      on_delete: :delete_all

    has_many :access_tokens, Linkhut.Oauth.AccessToken,
      foreign_key: :resource_owner_id,
      references: :id,
      on_delete: :delete_all

    timestamps(type: :utc_datetime)
  end

M lib/linkhut_web/controllers/settings/profile_controller.ex => lib/linkhut_web/controllers/settings/profile_controller.ex +22 -0
@@ 21,6 21,28 @@ defmodule LinkhutWeb.Settings.ProfileController do
    |> update(conn.assigns[:current_user], user_params)
  end

  require Logger

  def delete(conn, %{"user" => user_params}) do
    user = conn.assigns[:current_user]

    case Accounts.delete_user(user, user_params) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "Deleted account for #{user.username}")
        |> redirect(to: "/")
        |> configure_session(drop: true)

      {:error, changeset} ->
        conn
        |> render("profile.html",
          user: user,
          changeset: changeset,
          current_email_unconfirmed?: Accounts.current_email_unconfirmed?(user)
        )
    end
  end

  defp update(conn, user, params) when not is_nil(user) do
    case Accounts.update_user(user, params) do
      {:ok, user} ->

M lib/linkhut_web/router.ex => lib/linkhut_web/router.ex +1 -0
@@ 116,6 116,7 @@ defmodule LinkhutWeb.Router do
    get "/misc", MiscController, :show
    get "/profile", ProfileController, :show
    put "/profile", ProfileController, :update
    put "/profile/delete", ProfileController, :delete

    post "/confirm", EmailConfirmationController, :create
  end

M lib/linkhut_web/templates/settings/oauth/authorized_applications/_index.html.heex => lib/linkhut_web/templates/settings/oauth/authorized_applications/_index.html.heex +1 -1
@@ 17,7 17,7 @@
          <td><%= prettify(List.first(application.access_tokens).inserted_at) %></td>
          <td><%= prettify(NaiveDateTime.add(List.last(application.access_tokens).inserted_at, List.last(application.access_tokens).expires_in, :second)) %></td>
          <td>
            <%= form_for %{uid: application.uid}, Routes.oauth_path(@conn, :revoke_access, application.uid), [class: "inline"], fn f -> %>
            <%= form_for %{"uid" => application.uid}, Routes.oauth_path(@conn, :revoke_access, application.uid), [class: "inline"], fn f -> %>
              <%= hidden_input(f, :uid) %>
              <%= submit("Revoke") %>
            <% end %>

M lib/linkhut_web/templates/settings/profile.html.heex => lib/linkhut_web/templates/settings/profile.html.heex +10 -0
@@ 30,4 30,14 @@
      <%= submit("Save") %>
    <% end %>
  </section>
  <section class="settings">
    <h4>Delete Account</h4>
    <%= form_for @changeset, Routes.profile_path(@conn, :delete), fn f -> %>
      <fieldset>
        <%= input(f, :confirmed, type: :checkbox, label: "I acknowledge that I want to permanently delete my account and all data associated with it") %>
        <%= error_tag(f, :applications) %>
      </fieldset>
      <%= submit("Delete") %>
    <% end %>
  </section>
</div>

M lib/linkhut_web/views/error_helpers.ex => lib/linkhut_web/views/error_helpers.ex +13 -0
@@ 5,6 5,19 @@ defmodule LinkhutWeb.ErrorHelpers do
  use Phoenix.HTML

  @doc """
  Generates tag for inlined form input errors.
  """
  def error_tag(form, field) do
    if error = form.errors[field] do
      content_tag(
        :div,
        content_tag(:ul, content_tag(:li, translate_error(error), class: "invalid")),
        class: "invalid"
      )
    end
  end

  @doc """
  Translates an error message using gettext.
  """
  def translate_error({msg, opts}) do

M lib/linkhut_web/views/settings_view.ex => lib/linkhut_web/views/settings_view.ex +2 -0
@@ 2,6 2,8 @@ defmodule LinkhutWeb.SettingsView do
  use LinkhutWeb, :view

  use Phoenix.HTML
  import LinkhutWeb.FormHelpers
  import LinkhutWeb.ErrorHelpers

  @doc """
  Generates a bookmarklet link to add the current page to this linkhut instance