Skip to content

Commit 64f9d9c

Browse files
authored
data deletion oneshot (#691)
1 parent f05f0e2 commit 64f9d9c

File tree

19 files changed

+615
-18
lines changed

19 files changed

+615
-18
lines changed

app/assets/stylesheets/nav.css

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ body {
55

66
main {
77
flex: 1;
8-
margin-left: 250px;
9-
max-width: calc(100% - 250px);
108
padding: 20px;
119
margin-bottom: 100px;
1210
transition: margin-left 0.3s ease, max-width 0.3s ease;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
class Admin::DeletionRequestsController < Admin::BaseController
2+
before_action :set_deletion_request, only: [ :show, :approve, :reject ]
3+
before_action :require_admin, only: [ :index, :show, :approve, :reject ]
4+
5+
def index
6+
@pending = DeletionRequest.pending.includes(:user).order(requested_at: :asc)
7+
@approved = DeletionRequest.approved.includes(:user, :admin_approved_by).order(scheduled_deletion_at: :asc)
8+
@done = DeletionRequest.completed.includes(:user, :admin_approved_by).order(completed_at: :desc).limit(25)
9+
end
10+
11+
def show
12+
end
13+
14+
def approve
15+
@deletion_request.approve!(current_user)
16+
redirect_to admin_deletion_requests_path, notice: "they gonna go kerblam on #{@deletion_request.scheduled_deletion_at.strftime('%B %d, %Y')}."
17+
end
18+
19+
def reject
20+
@deletion_request.cancel!
21+
redirect_to admin_deletion_requests_path, notice: "ratioed + stay mad"
22+
end
23+
24+
private
25+
26+
def set_deletion_request
27+
@deletion_request = DeletionRequest.find(params[:id])
28+
end
29+
30+
def require_admin
31+
unless current_user && current_user.admin_level.in?([ "superadmin" ])
32+
redirect_to root_path, alert: "no perms lmaooo"
33+
end
34+
end
35+
end

app/controllers/api/hackatime/v1/hackatime_controller.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
class Api::Hackatime::V1::HackatimeController < ApplicationController
22
before_action :set_user
33
skip_before_action :verify_authenticity_token
4+
skip_before_action :enforce_lockout
5+
before_action :check_lockout, only: [ :push_heartbeats ]
46
before_action :set_raw_heartbeat_upload, only: [ :push_heartbeats ], if: :is_blank?
57

68
def push_heartbeats
@@ -309,6 +311,11 @@ def queue_heartbeat_public_activity(user_id, project_name)
309311
Rails.logger.error("Error queuing heartbeat public activity: #{e.class.name} #{e.message}")
310312
end
311313

314+
def check_lockout
315+
return unless @user&.pending_deletion?
316+
render json: { error: "Account pending deletion" }, status: :forbidden
317+
end
318+
312319
def set_user
313320
api_header = request.headers["Authorization"]
314321
raw_token = api_header&.split(" ")&.last

app/controllers/application_controller.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ class ApplicationController < ActionController::Base
55
before_action :try_rack_mini_profiler_enable
66
before_action :track_request
77
before_action :set_public_activity
8+
before_action :enforce_lockout
89
after_action :track_action
910

1011
around_action :switch_time_zone, if: :current_user
@@ -64,6 +65,12 @@ def authenticate_user!
6465
end
6566
end
6667

68+
def enforce_lockout
69+
return unless current_user&.pending_deletion?
70+
return if %w[deletion_requests sessions].include?(controller_name)
71+
redirect_to deletion_path
72+
end
73+
6774
def initialize_cache_counters
6875
Thread.current[:cache_hits] = 0
6976
Thread.current[:cache_misses] = 0
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
class DeletionRequestsController < ApplicationController
2+
before_action :require_login
3+
before_action :check_can_request, only: [ :create ]
4+
5+
def show
6+
@deletion_request = current_user.active_deletion_request
7+
redirect_to root_path, alert: "no request" unless @deletion_request
8+
end
9+
10+
def create
11+
@deletion_request = DeletionRequest.create_for_user!(current_user)
12+
redirect_to deletion_path
13+
rescue ActiveRecord::RecordInvalid => e
14+
Sentry.capture_exception(e)
15+
redirect_to my_settings_path
16+
end
17+
18+
def cancel
19+
@deletion_request = current_user.active_deletion_request
20+
if @deletion_request&.can_be_cancelled?
21+
@deletion_request.cancel!
22+
redirect_to my_settings_path, notice: "Your deletion request has been cancelled!"
23+
else
24+
redirect_to deletion_path
25+
end
26+
end
27+
28+
private
29+
30+
def require_login
31+
redirect_to root_path, alert: "who?" unless current_user
32+
end
33+
34+
def check_can_request
35+
unless current_user.can_request_deletion?
36+
redirect_to my_settings_path, alert: "You can't request deletion right now."
37+
end
38+
end
39+
end
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Controller } from "@hotwired/stimulus";
2+
3+
export default class extends Controller {
4+
connect() {
5+
console.log("AccountDeletionController connected");
6+
}
7+
8+
confirm(event) {
9+
event.preventDefault();
10+
console.log("AccountDeletionController#confirm called");
11+
const modal = document.getElementById("account-deletion-confirm-modal");
12+
if (modal) {
13+
modal.dispatchEvent(new CustomEvent("modal:open", { bubbles: true }));
14+
} else {
15+
console.error("Modal not found: account-deletion-confirm-modal");
16+
}
17+
}
18+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
class ProcessAccountDeletionsJob < ApplicationJob
2+
queue_as :default
3+
4+
def perform
5+
DeletionRequest.ready_for_deletion.find_each do |deletion_request|
6+
Rails.logger.info "kerblamming ##{deletion_request.user_id}"
7+
8+
begin
9+
AnonymizeUserService.call(deletion_request.user)
10+
deletion_request.complete!
11+
12+
Rails.logger.info "kerblamed account ##{deletion_request.user_id}"
13+
rescue StandardError => e
14+
Sentry.capture_exception(e, extra: { user_id: deletion_request.user_id })
15+
Rails.logger.error "failed to kerblam ##{deletion_request.user_id}: #{e.message}"
16+
Rails.logger.error e.backtrace.join("\n")
17+
end
18+
end
19+
end
20+
end

app/models/deletion_request.rb

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
class DeletionRequest < ApplicationRecord
2+
belongs_to :user
3+
belongs_to :admin_approved_by, class_name: "User", optional: true
4+
5+
enum :status, {
6+
pending: 0,
7+
approved: 1,
8+
cancelled: 2,
9+
completed: 3
10+
}
11+
12+
validates :requested_at, presence: true
13+
validate :user_not_banned_from_deletion, on: :create
14+
15+
scope :active, -> { where(status: [ :pending, :approved ]) }
16+
scope :ready_for_deletion, -> { approved.where("scheduled_deletion_at <= ?", Time.current) }
17+
18+
def self.create_for_user!(user)
19+
create!(
20+
user: user,
21+
requested_at: Time.current,
22+
status: :pending
23+
)
24+
end
25+
26+
def approve!(admin)
27+
update!(
28+
status: :approved,
29+
admin_approved_by: admin,
30+
admin_approved_at: Time.current,
31+
scheduled_deletion_at: Time.current + 30.days # grace period, if shit changes, change this
32+
)
33+
end
34+
35+
def cancel!
36+
update!(
37+
status: :cancelled,
38+
cancelled_at: Time.current
39+
)
40+
end
41+
42+
def complete!
43+
update!(
44+
status: :completed,
45+
completed_at: Time.current
46+
)
47+
end
48+
49+
def days_until_deletion
50+
return nil unless scheduled_deletion_at.present?
51+
[ (scheduled_deletion_at.to_date - Date.current).to_i, 0 ].max
52+
end
53+
54+
def can_be_cancelled?
55+
pending? || approved?
56+
end
57+
58+
private
59+
60+
def user_not_banned_from_deletion
61+
return unless user.present?
62+
63+
if user.red?
64+
last_audit = user.trust_level_audit_logs.order(created_at: :desc).first
65+
if last_audit && last_audit.created_at > 365.days.ago
66+
errors.add(:base, "You can not request data deletion due to a recent ban")
67+
end
68+
end
69+
end
70+
end

app/models/email_address.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ class EmailAddress < ApplicationRecord
99
enum :source, {
1010
signing_in: 0,
1111
github: 1,
12-
slack: 2
12+
slack: 2,
13+
preserved_for_deletion: 3
1314
}, prefix: true
1415

1516
before_validation :downcase_email

app/models/user.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ def set_trust(level, changed_by_user: nil, reason: nil, notes: nil)
118118

119119
has_many :trust_level_audit_logs, dependent: :destroy
120120
has_many :trust_level_changes_made, class_name: "TrustLevelAuditLog", foreign_key: "changed_by_id", dependent: :destroy
121+
has_many :deletion_requests, dependent: :destroy
122+
has_many :deletion_approvals, class_name: "DeletionRequest", foreign_key: "admin_approved_by_id"
121123

122124
has_many :access_grants,
123125
class_name: "Doorkeeper::AccessGrant",
@@ -133,6 +135,24 @@ def streak_days
133135
@streak_days ||= heartbeats.daily_streaks_for_users([ id ]).values.first
134136
end
135137

138+
def active_deletion_request
139+
deletion_requests.active.order(created_at: :desc).first
140+
end
141+
142+
def pending_deletion?
143+
active_deletion_request.present?
144+
end
145+
146+
def can_request_deletion?
147+
return false if pending_deletion?
148+
return true unless red?
149+
150+
last_audit = trust_level_audit_logs.order(created_at: :desc).first
151+
return true unless last_audit
152+
153+
last_audit.created_at <= 365.days.ago
154+
end
155+
136156
if Rails.env.development?
137157
def self.slow_find_by_email(email)
138158
# This is an n+1 query, but provided for developer convenience

0 commit comments

Comments
 (0)