diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 51f01728b..f91c708c0 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -2,7 +2,7 @@ module Admin class UsersController < Admin::BaseController layout 'admin', except: :js_search - before_action :get_user, only: [:edit, :update, :destroy, :become] + before_action :get_user, only: [:edit, :update, :become, :soft_delete] # Used by dialog def js_search @@ -42,12 +42,14 @@ def update end end - def destroy - salesforce_ids = [@user.salesforce_contact_id, @user.salesforce_lead_id].collect(&:to_s) - security_log :user_deleted_by_admin, user_id: params[:id], uuid: @user.uuid, salesforce_ids: salesforce_ids - @user.destroy + def soft_delete + result = SoftDeleteUser.call(@user) + + security_log :user_deleted_by_admin, user: @user, admin_id: @current_user.id + + # redirect_to admin_users_path + flash[:alert] = "Authentications and PII removed from account." redirect_to admin_users_path - flash[:alert] = "User account has been deleted" end def become diff --git a/app/models/application_user.rb b/app/models/application_user.rb index fe693a0a5..75eb2fc6c 100644 --- a/app/models/application_user.rb +++ b/app/models/application_user.rb @@ -1,7 +1,7 @@ class ApplicationUser < ApplicationRecord belongs_to :application, class_name: 'Doorkeeper::Application', inverse_of: :application_users - belongs_to :user, inverse_of: :application_users + belongs_to :user, inverse_of: :application_users, optional: true belongs_to :default_contact_info, class_name: 'ContactInfo' diff --git a/app/models/authentication.rb b/app/models/authentication.rb index 6415cfdc9..797cb6726 100644 --- a/app/models/authentication.rb +++ b/app/models/authentication.rb @@ -1,5 +1,7 @@ +include UserSessionManagement + class Authentication < ApplicationRecord - belongs_to :user, inverse_of: :authentications + belongs_to :user, inverse_of: :authentications, optional: true validates :provider, presence: true, uniqueness: { scope: :user_id }, @@ -20,6 +22,8 @@ def display_name protected def check_not_last + return if user.is_deleted? + if user.present? && user.authentications.size <= 1 && user.activated? throw(:abort) end diff --git a/app/models/contact_info.rb b/app/models/contact_info.rb index 7a794471b..d626711be 100644 --- a/app/models/contact_info.rb +++ b/app/models/contact_info.rb @@ -63,7 +63,7 @@ def strip end def check_if_last_verified - if verified? and not user.contact_infos.verified.many? and not destroyed_by_association + if verified? and not user.contact_infos.verified.many? and not destroyed_by_association and not user.is_deleted? errors.add(:user, :last_verified) throw(:abort) end diff --git a/app/routines/soft_delete_user.rb b/app/routines/soft_delete_user.rb new file mode 100644 index 000000000..ce550abcd --- /dev/null +++ b/app/routines/soft_delete_user.rb @@ -0,0 +1,33 @@ +class SoftDeleteUser + + lev_routine + + protected + + def exec(user) + return if user.nil? + + # Make sure object up to date, esp before dependent destroy stuff kicks in + user.reload + + user.is_deleted = true + user.save! + + user.external_ids.destroy_all + user.external_uuids.destroy_all + user.authentications.destroy_all + user.application_users.destroy_all + user.contact_infos.destroy_all + user.save! + + user.reload + user.first_name = 'Deleted' + user.last_name = 'User' + user.save! + + # security logs are read-only, but they contain PII so we force delete them for the user + user.security_logs.delete_all + + end + +end diff --git a/app/views/admin/users/_form.html.erb b/app/views/admin/users/_form.html.erb index d211bc9b1..09de326b9 100644 --- a/app/views/admin/users/_form.html.erb +++ b/app/views/admin/users/_form.html.erb @@ -50,6 +50,12 @@ <%= form_for [:admin, @user], html: {id: 'admin-user-form', class: 'form-horizontal'} do |f| %> <%= render "shared/error_messages", :target => @user %> + <% if @user.is_deleted? %> + + <% end %> +
<%= f.label :name, class: "col-sm-2 control-label" %>
@@ -401,8 +407,8 @@
- <%= link_to('Destroy User Account', - admin_user_path(@user), + <%= link_to('De-identify User Account', + soft_delete_admin_user_path(@user), class: 'btn btn-danger pull-right', style: 'margin-top: -50px; padding-top: -50px;', data: { diff --git a/config/routes.rb b/config/routes.rb index ed150c196..5463868b2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -285,13 +285,14 @@ put 'cron', to: 'base#cron' get 'raise_exception/:type', to: 'base#raise_exception', as: 'raise_exception' - resources :users, only: [:index, :update, :edit, :destroy] do + resources :users, only: [:index, :update, :edit] do post 'become', on: :member get 'search', on: :collection get 'js_search', on: :collection get 'actions', on: :collection put 'mark_users_updated', on: :collection post 'force_update_lead', on: :member + delete 'soft_delete', on: :member end resource :reports, only: [:show] diff --git a/db/migrate/20241009175157_add_delete_flag_to_user.rb b/db/migrate/20241009175157_add_delete_flag_to_user.rb new file mode 100644 index 000000000..66de1897d --- /dev/null +++ b/db/migrate/20241009175157_add_delete_flag_to_user.rb @@ -0,0 +1,5 @@ +class AddDeleteFlagToUser < ActiveRecord::Migration[5.2] + def change + add_column :users, :is_deleted, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 25d004d8e..bfbb193c7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2024_08_08_171751) do +ActiveRecord::Schema.define(version: 2024_10_09_175157) do # These are extensions that must be enabled in order to support this database enable_extension "citext" @@ -361,6 +361,7 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "country", default: "United States", null: false + t.boolean "has_assignable_contacts" t.index ["name", "city", "state"], name: "index_schools_on_name_and_city_and_state", opclass: :gist_trgm_ops, using: :gist t.index ["salesforce_id"], name: "index_schools_on_salesforce_id", unique: true t.index ["sheerid_school_name"], name: "index_schools_on_sheerid_school_name" @@ -472,6 +473,7 @@ t.jsonb "books_used_details" t.string "adopter_status" t.jsonb "consent_preferences" + t.boolean "is_deleted" t.index "lower((first_name)::text)", name: "index_users_on_first_name" t.index "lower((last_name)::text)", name: "index_users_on_last_name" t.index "lower((username)::text)", name: "index_users_on_username_case_insensitive"