1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-19 05:09:38 +02:00

Add ability to delete Maybe account (#698)

* Build out user deactivation and purging workflows

* Add i18n translations for user deletion

* Add tests for user deletion

* Fix lint issue
This commit is contained in:
Josh Brown 2024-04-30 16:40:31 +01:00 committed by GitHub
parent 55cb1ae5bd
commit 19ee773d9b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 128 additions and 8 deletions

View file

@ -17,6 +17,15 @@ class Settings::ProfilesController < ApplicationController
end end
end end
def destroy
if Current.user.deactivate
logout
redirect_to root_path, notice: t(".success")
else
redirect_to settings_profile_path, alert: Current.user.errors.full_messages.to_sentence
end
end
private private
def user_params def user_params

View file

@ -10,7 +10,7 @@ Turbo.setConfirmMethod((message) => {
const dialog = document.getElementById("turbo-confirm"); const dialog = document.getElementById("turbo-confirm");
try { try {
const { title, body, accept } = JSON.parse(message); const { title, body, accept, acceptClass } = JSON.parse(message);
if (title) { if (title) {
document.getElementById("turbo-confirm-title").innerHTML = title; document.getElementById("turbo-confirm-title").innerHTML = title;
@ -23,6 +23,10 @@ Turbo.setConfirmMethod((message) => {
if (accept) { if (accept) {
document.getElementById("turbo-confirm-accept").innerHTML = accept; document.getElementById("turbo-confirm-accept").innerHTML = accept;
} }
if (acceptClass) {
document.getElementById("turbo-confirm-accept").className = acceptClass;
}
} catch (e) { } catch (e) {
document.getElementById("turbo-confirm-title").innerText = message; document.getElementById("turbo-confirm-title").innerText = message;
} }

View file

@ -0,0 +1,7 @@
class UserPurgeJob < ApplicationJob
queue_as :default
def perform(user)
user.purge
end
end

View file

@ -38,4 +38,40 @@ class User < ApplicationRecord
def has_seen_upgrade_alert?(upgrade) def has_seen_upgrade_alert?(upgrade)
last_alerted_upgrade_commit_sha == upgrade.commit_sha last_alerted_upgrade_commit_sha == upgrade.commit_sha
end end
# Deactivation
validate :can_deactivate, if: -> { active_changed? && !active }
after_update_commit :purge_later, if: -> { saved_change_to_active?(from: true, to: false) }
def deactivate
update active: false, email: deactivated_email
end
def can_deactivate
if admin? && family.users.count > 1
errors.add(:base, I18n.t("activerecord.errors.user.cannot_deactivate_admin_with_other_users"))
end
end
def purge_later
UserPurgeJob.perform_later(self)
end
def purge
if last_user_in_family?
family.destroy
else
destroy
end
end
private
def last_user_in_family?
family.users.count == 1
end
def deactivated_email
email.gsub(/@/, "-deactivated-#{SecureRandom.uuid}@")
end
end end

View file

@ -61,9 +61,16 @@
<h3 class="font-medium text-gray-900"><%= t(".delete_account") %></h3> <h3 class="font-medium text-gray-900"><%= t(".delete_account") %></h3>
<p class="text-gray-500 text-sm"><%= t(".delete_account_warning") %></p> <p class="text-gray-500 text-sm"><%= t(".delete_account_warning") %></p>
</div> </div>
<button disabled class="bg-red-500 text-white text-sm font-medium rounded-lg px-3 py-2 cursor-not-allowed"> <%=
<%= t(".delete_account") %> button_to t(".delete_account"), settings_profile_path, method: :delete,
</button> class: "bg-red-500 text-white text-sm font-medium rounded-lg px-3 py-2",
data: { turbo_confirm: {
title: t(".confirm_delete.title"),
body: t(".confirm_delete.body"),
accept: t(".delete_account"),
acceptClass: "w-full bg-red-500 text-white rounded-xl text-center p-[10px] border mb-2"
}}
%>
</div> </div>
<% end %> <% end %>
</div> </div>

View file

@ -11,6 +11,7 @@
<%= t(".body_html") %> <%= t(".body_html") %>
</div> </div>
</div> </div>
<button id="turbo-confirm-accept" class="w-full text-red-600 rounded-xl text-center p-[10px] border" value="confirm"><%= t(".accept") %></button> <button id="turbo-confirm-accept" class="w-full text-red-600 rounded-xl text-center p-[10px] border mb-2" value="confirm"><%= t(".accept") %></button>
<button class="w-full rounded-xl text-center p-[10px] border" value="cancel"><%= t(".cancel") %></button>
</form> </form>
</dialog> </dialog>

View file

@ -10,3 +10,7 @@ en:
last_name: Last Name last_name: Last Name
password: Password password: Password
password_confirmation: Password Confirmation password_confirmation: Password Confirmation
errors:
user:
cannot_deactivate_admin_with_other_users: Admin cannot delete account while
other users are present. Please delete all members first.

View file

@ -82,8 +82,14 @@ en:
update: update:
success: Preferences updated successfully. success: Preferences updated successfully.
profiles: profiles:
destroy:
success: Account deleted successfully.
show: show:
add_member: Add Member add_member: Add Member
confirm_delete:
body: Are you sure you want to permanently delete your account? This action
is irreversible.
title: Delete account?
danger_zone_title: Danger Zone danger_zone_title: Danger Zone
delete_account: Delete Account delete_account: Delete Account
delete_account_warning: Deleting your account will permanently remove all delete_account_warning: Deleting your account will permanently remove all

View file

@ -4,6 +4,7 @@ en:
confirm_modal: confirm_modal:
accept: Confirm accept: Confirm
body_html: "<p>You will not be able to undo this decision</p>" body_html: "<p>You will not be able to undo this decision</p>"
cancel: Cancel
title: Are you sure? title: Are you sure?
notification: notification:
dismiss: Dismiss dismiss: Dismiss

View file

@ -11,7 +11,7 @@ Rails.application.routes.draw do
resource :password resource :password
namespace :settings do namespace :settings do
resource :profile, only: %i[show update] resource :profile, only: %i[show update destroy]
resource :preferences, only: %i[show update] resource :preferences, only: %i[show update]
resource :notifications, only: %i[show update] resource :notifications, only: %i[show update]
resource :billing, only: %i[show update] resource :billing, only: %i[show update]

View file

@ -0,0 +1,5 @@
class AddActiveToUsers < ActiveRecord::Migration[7.2]
def change
add_column :users, :active, :boolean, default: true, null: false
end
end

5
db/schema.rb generated
View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2024_04_26_191312) do ActiveRecord::Schema[7.2].define(version: 2024_04_30_111641) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto" enable_extension "pgcrypto"
enable_extension "plpgsql" enable_extension "plpgsql"
@ -86,7 +86,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_04_26_191312) do
t.uuid "accountable_id" t.uuid "accountable_id"
t.decimal "balance", precision: 19, scale: 4, default: "0.0" t.decimal "balance", precision: 19, scale: 4, default: "0.0"
t.string "currency", default: "USD" t.string "currency", default: "USD"
t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY (ARRAY[('Account::Loan'::character varying)::text, ('Account::Credit'::character varying)::text, ('Account::OtherLiability'::character varying)::text])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY ((ARRAY['Account::Loan'::character varying, 'Account::Credit'::character varying, 'Account::OtherLiability'::character varying])::text[])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
t.boolean "is_active", default: true, null: false t.boolean "is_active", default: true, null: false
t.enum "status", default: "ok", null: false, enum_type: "account_status" t.enum "status", default: "ok", null: false, enum_type: "account_status"
t.jsonb "sync_warnings", default: "[]", null: false t.jsonb "sync_warnings", default: "[]", null: false
@ -257,6 +257,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_04_26_191312) do
t.string "last_prompted_upgrade_commit_sha" t.string "last_prompted_upgrade_commit_sha"
t.string "last_alerted_upgrade_commit_sha" t.string "last_alerted_upgrade_commit_sha"
t.enum "role", default: "member", null: false, enum_type: "user_role" t.enum "role", default: "member", null: false, enum_type: "user_role"
t.boolean "active", default: true, null: false
t.index ["email"], name: "index_users_on_email", unique: true t.index ["email"], name: "index_users_on_email", unique: true
t.index ["family_id"], name: "index_users_on_family_id" t.index ["family_id"], name: "index_users_on_family_id"
end end

View file

@ -4,8 +4,39 @@ class Settings::ProfilesControllerTest < ActionDispatch::IntegrationTest
setup do setup do
sign_in @user = users(:family_admin) sign_in @user = users(:family_admin)
end end
test "get" do test "get" do
get settings_profile_url get settings_profile_url
assert_response :success assert_response :success
end end
test "member can deactivate their account" do
sign_in @member = users(:family_member)
delete settings_profile_url
assert_redirected_to root_url
assert_not User.find(@member.id).active?
assert_enqueued_with(job: UserPurgeJob, args: [ @member ])
end
test "admin prevented from deactivating when other users are present" do
sign_in @admin = users(:family_admin)
delete settings_profile_url
assert_redirected_to settings_profile_url
assert_no_enqueued_jobs only: UserPurgeJob
assert User.find(@admin.id).active?
end
test "admin can deactivate their account when they are the last user in the family" do
sign_in @admin = users(:family_admin)
users(:family_member).destroy
delete settings_profile_url
assert_redirected_to root_url
assert_not User.find(@admin.id).active?
assert_enqueued_with(job: UserPurgeJob, args: [ @admin ])
end
end end

View file

@ -4,6 +4,7 @@ family_admin:
last_name: Dylan last_name: Dylan
email: bob@bobdylan.com email: bob@bobdylan.com
password_digest: <%= BCrypt::Password.create('password') %> password_digest: <%= BCrypt::Password.create('password') %>
role: admin
family_member: family_member:
family: dylan_family family: dylan_family

View file

@ -0,0 +1,7 @@
require "test_helper"
class UserPurgeJobTest < ActiveJob::TestCase
# test "the truth" do
# assert true
# end
end