Skip to content
Open
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
42 changes: 42 additions & 0 deletions app/jobs/audits_backfill_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Job to fill the user_past_usernames and user_past_emails tables using information from the audits table
class AuditsBackfillJob < RedisSetJob
queue_as :utilities

def self.base_key
"audits_backfill"
end

def self.job_size
1000
end

def self.batch_size
100
end

def perform_on_batch(user_ids)
# scans by users so audits can be taken from users and limited appropriately
User.where(id: user_ids).find_each do |user|
# gets past data audits within limits
past_data = user.audits.order(id: :desc).where(action: "update").limit(ArchiveConfig.USER_HISTORIC_VALUES_LIMIT).filter_map do |audit|
if audit.audited_changes.key?("login") && audit.audited_changes["login"].first.present?
{ username: audit.audited_changes["login"].first, changed_at: audit.created_at }
elsif audit.audited_changes.key?("email") && audit.audited_changes["email"].first.present?
{ email: audit.audited_changes["email"].first, changed_at: audit.created_at }
end
end
past_data = past_data.uniq { |audit| audit[:username] }
.reject { |audit| audit[:username] == user.login }
past_data = past_data.uniq { |audit| audit[:email] }
.reject { |audit| audit[:email] == user.email }
# adds each type of past data to the appropriate database table
past_data.each do |audit|
window = audit[:changed_at] - 60.seconds..audit[:changed_at] + 60.seconds
UserPastUsername.create!(user_id: user.id, username: audit[:username], changed_at: audit[:changed_at]) if audit.key?(:username) && !UserPastUsername.exists?(user_id: user.id, username: audit[:username], changed_at: window)
next unless audit.key?(:email)

UserPastEmail.create!(user_id: user.id, email_address: audit[:email], changed_at: audit[:changed_at]) unless UserPastEmail.exists?(user_id: user.id, email_address: audit[:email], changed_at: window)
end
end
end
end
15 changes: 14 additions & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,17 @@ class User < ApplicationRecord
has_many :skins, foreign_key: "author_id", dependent: :nullify
has_many :work_skins, foreign_key: "author_id", dependent: :nullify

# the user's past credentials
has_many :user_past_emails, dependent: :destroy
has_many :user_past_usernames, dependent: :destroy

before_update :add_renamed_at, if: :will_save_change_to_login?
after_update :update_pseud_name
after_update :send_wrangler_username_change_notification, if: :is_tag_wrangler?
after_update :log_change_if_login_was_edited, if: :saved_change_to_login?
after_update :log_email_change, if: :saved_change_to_email?
after_update :expire_caches
before_destroy :remove_user_from_kudos

# Extra callback to make sure readings are deleted in an order consistent
# with the ReadingsJob.
#
Expand Down Expand Up @@ -197,6 +200,14 @@ def unread_inbox_comments_count
unread_inbox_comments.with_bad_comments_removed.count
end

def past_emails
self.user_past_emails
end

def past_usernames
self.user_past_usernames
end

scope :alphabetical, -> { order(:login) }
scope :starting_with, ->(letter) { where("login like ?", "#{letter}%") }
scope :valid, -> { where(banned: false, suspended: false) }
Expand Down Expand Up @@ -584,6 +595,7 @@ def log_change_if_login_was_edited
else
"Old Username: #{login_before_last_save}; New Username: #{login}"
end
user_past_usernames.create!(username: login_before_last_save, changed_at: self.updated_at)
create_log_item(options)
end

Expand All @@ -600,6 +612,7 @@ def log_email_change
admin_id: current_admin&.id
}
options[:note] = "Change made by #{current_admin&.login}" if current_admin
user_past_emails.create!(email_address: email_before_last_save, changed_at: self.updated_at)
create_log_item(options)
end

Expand Down
4 changes: 4 additions & 0 deletions app/models/user_past_email.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Allows admins to track all past emails of a user
class UserPastEmail < ApplicationRecord
belongs_to :user
end
4 changes: 4 additions & 0 deletions app/models/user_past_username.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Allows admins to track all past usernames of a user
class UserPastUsername < ApplicationRecord
belongs_to :user
end
12 changes: 12 additions & 0 deletions db/migrate/20260103083335_create_user_past_emails.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class CreateUserPastEmails < ActiveRecord::Migration[7.2]
def change
create_table :user_past_emails do |t|
t.references :user, null: false, foreign_key: true, type: :integer

t.string :email_address
t.datetime :changed_at

t.timestamps
end
end
end
12 changes: 12 additions & 0 deletions db/migrate/20260103083447_create_user_past_usernames.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class CreateUserPastUsernames < ActiveRecord::Migration[7.2]
def change
create_table :user_past_usernames do |t|
t.references :user, null: false, foreign_key: true, type: :integer

t.string :username
t.datetime :changed_at

t.timestamps
end
end
end
12 changes: 12 additions & 0 deletions lib/tasks/after_tasks.rake
Original file line number Diff line number Diff line change
Expand Up @@ -629,5 +629,17 @@ namespace :After do
end
puts "Job complete."
end

desc "Run backfill for user_past_emails and user_past_usernames"
task(backfill: :environment) do
User.find_in_batches.with_index do |batch, index|
REDIS_GENERAL.sadd?("audits_backfill", batch.map(&:id))

batch_number = index + 1
puts "Batch #{batch_number} complete."
end
AuditsBackfillJob.spawn_jobs
puts "Backfill started and running on resque in background"
end
# This is the end that you have to put new tasks above.
end
58 changes: 58 additions & 0 deletions spec/jobs/audits_backfill_job_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

require "spec_helper"

describe AuditsBackfillJob do
let(:existing_user) { create(:user) }

context "when audits previously exist" do
before do
# Manually create audit record so user_past_usernames isn't updated
existing_user.audits.create!(action: "update", auditable: existing_user, user: existing_user,
auditable_id: existing_user.id, audited_changes: { "login" => ["old_login", existing_user.login] })
existing_user.audits.create!(action: "update", auditable: existing_user, user: existing_user,
auditable_id: existing_user.id, audited_changes: { "email" => ["[email protected]", existing_user.email] })
end

it "creates backfilled records in user_past_usernames" do
user_ids = [existing_user.id]
AuditsBackfillJob.new.perform_on_batch(user_ids)
expect(existing_user.past_usernames.last.username).to eq("old_login")
end

it "creates backfilled records in user_past_emails" do
user_ids = [existing_user.id]
AuditsBackfillJob.new.perform_on_batch(user_ids)
expect(existing_user.past_emails.last.email_address).to eq("[email protected]")
end

it "contains both backfilled and new changes" do
user_ids = [existing_user.id]
AuditsBackfillJob.new.perform_on_batch(user_ids)
old_username = existing_user.login
existing_user.update!(login: "new_login")
expect(existing_user.past_usernames.count).to eq(2)
expect(existing_user.past_usernames.last.username).to eq(old_username)
expect(existing_user.past_usernames.first.username).to eq("old_login")
end
end

it "doesn't create duplicate records" do
# change with update first, then run backfill
old_username = existing_user.login
existing_user.update!(login: "new_login")
user_ids = [existing_user.id]
AuditsBackfillJob.new.perform_on_batch(user_ids)

expect(existing_user.past_usernames.count).to eq(1)
expect(existing_user.past_usernames.last.username).to eq(old_username)
end

it "handles audits with a empty field" do
existing_user.audits.create!(action: "update", auditable: existing_user, user: existing_user,
auditable_id: existing_user.id, audited_changes: { "email" => ["", existing_user.email] })
user_ids = [existing_user.id]
AuditsBackfillJob.new.perform_on_batch(user_ids)
expect(existing_user.past_emails.count).to eq(0)
end
end
24 changes: 24 additions & 0 deletions spec/models/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,15 @@
expect(existing_user.renamed_at).to eq(Time.current)
expect(existing_user.admin_renamed_at).to be nil
end

it "records past username" do
freeze_time
existing_user.update!(login: "new_username")
user_change = existing_user.past_usernames.last
expect(user_change.user_id).to eq(existing_user.id)
expect(user_change.username).not_to eq("new_username")
expect(user_change.changed_at).to eq(Time.current)
end

context "username was recently changed" do
before do
Expand Down Expand Up @@ -340,6 +349,21 @@
expect(log_item.admin_id).to be_nil
expect(log_item.note).to eq("System Generated")
end

it "records previous email" do
old_email = existing_user.past_emails.last
expect(old_email.email_address).not_to eq(existing_user.email)
expect(old_email.user_id).to eq(existing_user.id)
expect(old_email.changed_at).to eq(existing_user.updated_at)
end

it "doesn't record previous email before confirmation" do
old_email = existing_user.user_past_emails.last
existing_user.update!(email: "[email protected]")
existing_user.reload
after_email = existing_user.user_past_emails.last
expect(old_email).to eq(after_email)
end
end

context "as an admin" do
Expand Down
Loading