-
-
Notifications
You must be signed in to change notification settings - Fork 902
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Soft-delete users #4376
Soft-delete users #4376
Changes from all commits
8e1463d
7811892
f7a3554
ff9a57a
531f7d7
d07e105
fcdd533
5611221
34ac8aa
5ccbf1b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,28 @@ | ||
class User < ApplicationRecord | ||
include UserMultifactorMethods | ||
include Clearance::User | ||
|
||
include Gravtastic | ||
is_gravtastic default: "retro" | ||
|
||
include Discard::Model | ||
self.discard_column = :deleted_at | ||
|
||
default_scope { not_deleted } | ||
|
||
before_save :_generate_confirmation_token_no_reset_unconfirmed_email, if: :will_save_change_to_unconfirmed_email? | ||
before_create :_generate_confirmation_token_no_reset_unconfirmed_email | ||
before_discard :yank_gems | ||
before_discard :expire_all_api_keys | ||
before_discard :destroy_associations_for_discard | ||
before_discard :clear_personal_attributes | ||
after_discard :send_deletion_complete_email | ||
before_destroy :yank_gems | ||
|
||
scope :not_deleted, -> { kept } | ||
scope :deleted, -> { with_discarded.discarded } | ||
scope :with_deleted, -> { with_discarded } | ||
|
||
has_many :ownerships, -> { confirmed }, dependent: :destroy, inverse_of: :user | ||
|
||
has_many :rubygems, through: :ownerships, source: :rubygem | ||
|
@@ -25,7 +40,10 @@ class User < ApplicationRecord | |
has_many :api_keys, dependent: :destroy, inverse_of: :owner, as: :owner | ||
|
||
has_many :ownership_calls, -> { opened }, dependent: :destroy, inverse_of: :user | ||
has_many :closed_ownership_calls, -> { closed }, dependent: :destroy, inverse_of: :user, class_name: "OwnershipCall" | ||
has_many :ownership_requests, -> { opened }, dependent: :destroy, inverse_of: :user | ||
has_many :closed_ownership_requests, -> { closed }, dependent: :destroy, inverse_of: :user, class_name: "OwnershipRequest" | ||
has_many :approved_ownership_requests, -> { approved }, dependent: :destroy, inverse_of: :user, class_name: "OwnershipRequest" | ||
|
||
has_many :audits, as: :auditable, dependent: :nullify | ||
|
||
|
@@ -280,4 +298,37 @@ def toxic_email_domain | |
|
||
errors.add(:email, I18n.t("activerecord.errors.messages.blocked", domain: domain)) if toxic | ||
end | ||
|
||
def expire_all_api_keys | ||
api_keys.unexpired.expire_all! | ||
end | ||
|
||
def destroy_associations_for_discard | ||
ownerships.unscope(where: :confirmed_at).destroy_all | ||
ownership_requests.update_all(status: :closed) | ||
ownership_calls.unscope(where: :status).destroy_all | ||
oidc_pending_trusted_publishers.destroy_all | ||
subscriptions.destroy_all | ||
web_hooks.destroy_all | ||
webauthn_credentials.destroy_all | ||
webauthn_verification&.destroy! | ||
end | ||
Comment on lines
+306
to
+315
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is way better in my opinion. Absolutely easier to understand what will happen. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
|
||
def clear_personal_attributes | ||
@email_before_discard = email | ||
update!( | ||
email: "deleted+#{id}@rubygems.org", | ||
handle: nil, email_confirmed: false, | ||
unconfirmed_email: nil, blocked_email: nil, | ||
api_key: nil, confirmation_token: nil, remember_token: nil, | ||
twitter_username: nil, webauthn_id: nil, full_name: nil, | ||
totp_seed: nil, mfa_hashed_recovery_codes: nil, | ||
mfa_level: :disabled, | ||
password: SecureRandom.hex(20).encode("UTF-8") | ||
) | ||
end | ||
|
||
def send_deletion_complete_email | ||
Mailer.deletion_complete(@email_before_discard).deliver_later | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
class AddDeletedAtToUser < ActiveRecord::Migration[7.1] | ||
def change | ||
add_column :users, :deleted_at, :datetime | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would expect this logic to be part of model itself and in background job just got called something like
user.soft_delete
. Btw. would it make sense to rename this job toSoftDeleteUserJob
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, this solves the concern about a split canonical information about deletes.