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:
parent
55cb1ae5bd
commit
19ee773d9b
15 changed files with 128 additions and 8 deletions
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
7
app/jobs/user_purge_job.rb
Normal file
7
app/jobs/user_purge_job.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
class UserPurgeJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform(user)
|
||||||
|
user.purge
|
||||||
|
end
|
||||||
|
end
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
5
db/migrate/20240430111641_add_active_to_users.rb
Normal file
5
db/migrate/20240430111641_add_active_to_users.rb
Normal 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
5
db/schema.rb
generated
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
1
test/fixtures/users.yml
vendored
1
test/fixtures/users.yml
vendored
|
@ -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
|
||||||
|
|
7
test/jobs/user_purge_job_test.rb
Normal file
7
test/jobs/user_purge_job_test.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class UserPurgeJobTest < ActiveJob::TestCase
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
Loading…
Add table
Add a link
Reference in a new issue