Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions app/assets/stylesheets/nav.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ body {

main {
flex: 1;
margin-left: 250px;
max-width: calc(100% - 250px);
padding: 20px;
margin-bottom: 100px;
transition: margin-left 0.3s ease, max-width 0.3s ease;
Expand Down
35 changes: 35 additions & 0 deletions app/controllers/admin/deletion_requests_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
class Admin::DeletionRequestsController < Admin::BaseController
before_action :set_deletion_request, only: [ :show, :approve, :reject ]
before_action :require_admin, only: [ :index, :show, :approve, :reject ]

def index
@pending = DeletionRequest.pending.includes(:user).order(requested_at: :asc)
@approved = DeletionRequest.approved.includes(:user, :admin_approved_by).order(scheduled_deletion_at: :asc)
@done = DeletionRequest.completed.includes(:user, :admin_approved_by).order(completed_at: :desc).limit(25)
end

def show
end

def approve
@deletion_request.approve!(current_user)
redirect_to admin_deletion_requests_path, notice: "they gonna go kerblam on #{@deletion_request.scheduled_deletion_at.strftime('%B %d, %Y')}."
end

def reject
@deletion_request.cancel!
redirect_to admin_deletion_requests_path, notice: "ratioed + stay mad"
end

private

def set_deletion_request
@deletion_request = DeletionRequest.find(params[:id])
end

def require_admin
unless current_user && current_user.admin_level.in?([ "superadmin" ])
redirect_to root_path, alert: "no perms lmaooo"
end
end
end
7 changes: 7 additions & 0 deletions app/controllers/api/hackatime/v1/hackatime_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
class Api::Hackatime::V1::HackatimeController < ApplicationController
before_action :set_user
skip_before_action :verify_authenticity_token
skip_before_action :enforce_lockout
before_action :check_lockout, only: [ :push_heartbeats ]
before_action :set_raw_heartbeat_upload, only: [ :push_heartbeats ], if: :is_blank?

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

def check_lockout
return unless @user&.pending_deletion?
render json: { error: "Account pending deletion" }, status: :forbidden
end

def set_user
api_header = request.headers["Authorization"]
raw_token = api_header&.split(" ")&.last
Expand Down
7 changes: 7 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class ApplicationController < ActionController::Base
before_action :try_rack_mini_profiler_enable
before_action :track_request
before_action :set_public_activity
before_action :enforce_lockout
after_action :track_action

around_action :switch_time_zone, if: :current_user
Expand Down Expand Up @@ -64,6 +65,12 @@ def authenticate_user!
end
end

def enforce_lockout
return unless current_user&.pending_deletion?
return if %w[deletion_requests sessions].include?(controller_name)
redirect_to deletion_path
end

def initialize_cache_counters
Thread.current[:cache_hits] = 0
Thread.current[:cache_misses] = 0
Expand Down
39 changes: 39 additions & 0 deletions app/controllers/deletion_requests_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
class DeletionRequestsController < ApplicationController
before_action :require_login
before_action :check_can_request, only: [ :create ]

def show
@deletion_request = current_user.active_deletion_request
redirect_to root_path, alert: "no request" unless @deletion_request
end

def create
@deletion_request = DeletionRequest.create_for_user!(current_user)
redirect_to deletion_path
rescue ActiveRecord::RecordInvalid => e
Sentry.capture_exception(e)
redirect_to my_settings_path
end

def cancel
@deletion_request = current_user.active_deletion_request
if @deletion_request&.can_be_cancelled?
@deletion_request.cancel!
redirect_to my_settings_path, notice: "Your deletion request has been cancelled!"
else
redirect_to deletion_path
end
end

private

def require_login
redirect_to root_path, alert: "who?" unless current_user
end

def check_can_request
unless current_user.can_request_deletion?
redirect_to my_settings_path, alert: "You can't request deletion right now."
end
end
end
18 changes: 18 additions & 0 deletions app/javascript/controllers/account_deletion_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
connect() {
console.log("AccountDeletionController connected");
}

confirm(event) {
event.preventDefault();
console.log("AccountDeletionController#confirm called");
const modal = document.getElementById("account-deletion-confirm-modal");
if (modal) {
modal.dispatchEvent(new CustomEvent("modal:open", { bubbles: true }));
} else {
console.error("Modal not found: account-deletion-confirm-modal");
}
}
}
20 changes: 20 additions & 0 deletions app/jobs/process_account_deletions_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class ProcessAccountDeletionsJob < ApplicationJob
queue_as :default

def perform
DeletionRequest.ready_for_deletion.find_each do |deletion_request|
Rails.logger.info "kerblamming ##{deletion_request.user_id}"

begin
AnonymizeUserService.call(deletion_request.user)
deletion_request.complete!

Rails.logger.info "kerblamed account ##{deletion_request.user_id}"
rescue StandardError => e
Sentry.capture_exception(e, extra: { user_id: deletion_request.user_id })
Rails.logger.error "failed to kerblam ##{deletion_request.user_id}: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
end
end
end
end
70 changes: 70 additions & 0 deletions app/models/deletion_request.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
class DeletionRequest < ApplicationRecord
belongs_to :user
belongs_to :admin_approved_by, class_name: "User", optional: true

enum :status, {
pending: 0,
approved: 1,
cancelled: 2,
completed: 3
}

validates :requested_at, presence: true
validate :user_not_banned_from_deletion, on: :create

scope :active, -> { where(status: [ :pending, :approved ]) }
scope :ready_for_deletion, -> { approved.where("scheduled_deletion_at <= ?", Time.current) }

def self.create_for_user!(user)
create!(
user: user,
requested_at: Time.current,
status: :pending
)
end

def approve!(admin)
update!(
status: :approved,
admin_approved_by: admin,
admin_approved_at: Time.current,
scheduled_deletion_at: Time.current + 30.days # grace period, if shit changes, change this
)
end

def cancel!
update!(
status: :cancelled,
cancelled_at: Time.current
)
end

def complete!
update!(
status: :completed,
completed_at: Time.current
)
end

def days_until_deletion
return nil unless scheduled_deletion_at.present?
[ (scheduled_deletion_at.to_date - Date.current).to_i, 0 ].max
end

def can_be_cancelled?
pending? || approved?
end

private

def user_not_banned_from_deletion
return unless user.present?

if user.red?
last_audit = user.trust_level_audit_logs.order(created_at: :desc).first
if last_audit && last_audit.created_at > 365.days.ago
errors.add(:base, "You can not request data deletion due to a recent ban")
end
end
end
end
3 changes: 2 additions & 1 deletion app/models/email_address.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ class EmailAddress < ApplicationRecord
enum :source, {
signing_in: 0,
github: 1,
slack: 2
slack: 2,
preserved_for_deletion: 3
}, prefix: true

before_validation :downcase_email
Expand Down
20 changes: 20 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ def set_trust(level, changed_by_user: nil, reason: nil, notes: nil)

has_many :trust_level_audit_logs, dependent: :destroy
has_many :trust_level_changes_made, class_name: "TrustLevelAuditLog", foreign_key: "changed_by_id", dependent: :destroy
has_many :deletion_requests, dependent: :destroy
has_many :deletion_approvals, class_name: "DeletionRequest", foreign_key: "admin_approved_by_id"

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

def active_deletion_request
deletion_requests.active.order(created_at: :desc).first
end

def pending_deletion?
active_deletion_request.present?
end

def can_request_deletion?
return false if pending_deletion?
return true unless red?

last_audit = trust_level_audit_logs.order(created_at: :desc).first
return true unless last_audit

last_audit.created_at <= 365.days.ago
end

if Rails.env.development?
def self.slow_find_by_email(email)
# This is an n+1 query, but provided for developer convenience
Expand Down
70 changes: 70 additions & 0 deletions app/services/anonymize_user_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
class AnonymizeUserService
def self.call(user)
new(user).call
end

def initialize(user)
@user = user
end

def call
ActiveRecord::Base.transaction do
preserve_emails_for_ban_tracking
anonymize_user_data
destroy_associated_records
invalidate_sessions
end
end

private

attr_reader :user

def preserve_emails_for_ban_tracking
user.email_addresses.update_all(
user_id: user.id,
source: EmailAddress.sources[:preserved_for_deletion]
)
end

def anonymize_user_data
user.update!(
slack_uid: nil,
slack_username: nil,
slack_avatar_url: nil,
slack_access_token: nil,
slack_scopes: [],
slack_neighborhood_channel: nil,
github_uid: nil,
github_username: nil,
github_avatar_url: nil,
github_access_token: nil,
hca_id: nil,
hca_access_token: nil,
hca_scopes: [],
username: "deleted_user_#{user.id}",
uses_slack_status: false,
country_code: nil,
mailing_address_otc: nil,
deprecated_name: nil
)
end

def destroy_associated_records
user.api_keys.destroy_all
user.admin_api_keys.destroy_all
user.sign_in_tokens.destroy_all
user.email_verification_requests.destroy_all
user.wakatime_mirrors.destroy_all
user.project_repo_mappings.destroy_all
user.mailing_address&.destroy
user.heartbeats.destroy_all

user.access_grants.destroy_all
user.access_tokens.destroy_all
end

def invalidate_sessions
user.sign_in_tokens.destroy_all
end
end
Loading