loggable_activity 0.1.36
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.rspec +3 -0
- data/.rspec_status +0 -0
- data/.rubocop.yml +35 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONSIDERTIONS.md +129 -0
- data/GETTING-STARTED.md +80 -0
- data/LICENSE.txt +21 -0
- data/README.md +32 -0
- data/ROADMAP.md +19 -0
- data/Rakefile +8 -0
- data/lib/.DS_Store +0 -0
- data/lib/generators/.DS_Store +0 -0
- data/lib/generators/loggable_activity/.DS_Store +0 -0
- data/lib/generators/loggable_activity/install_generator.rb +30 -0
- data/lib/generators/loggable_activity/templates/.DS_Store +0 -0
- data/lib/generators/loggable_activity/templates/create_loggable_activities.rb +15 -0
- data/lib/generators/loggable_activity/templates/create_loggable_encryption_keys.rb +11 -0
- data/lib/generators/loggable_activity/templates/create_loggable_payloads.rb +15 -0
- data/lib/generators/loggable_activity/templates/current_user.rb +26 -0
- data/lib/generators/loggable_activity/templates/loggable_activity.en.yml +36 -0
- data/lib/loggable_activity/.DS_Store +0 -0
- data/lib/loggable_activity/activity.rb +124 -0
- data/lib/loggable_activity/configuration.rb +13 -0
- data/lib/loggable_activity/encryption.rb +43 -0
- data/lib/loggable_activity/encryption_key.rb +46 -0
- data/lib/loggable_activity/hooks.rb +152 -0
- data/lib/loggable_activity/payload.rb +71 -0
- data/lib/loggable_activity/payloads_builder.rb +89 -0
- data/lib/loggable_activity/update_payloads_builder.rb +99 -0
- data/lib/loggable_activity/version.rb +5 -0
- data/lib/loggable_activity.rb +18 -0
- data/loggable_activity-0.1.32.gem +0 -0
- data/loggable_activity-0.1.33.gem +0 -0
- data/loggable_activity-0.1.34.gem +0 -0
- data/pkg/loggable_activity-0.1.35.gem +0 -0
- data/sig/loggable_activity.rbs +4 -0
- metadata +105 -0
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This is the activity log. It contains an agregation of payloads.
|
4
|
+
# It reprecent one activity for the log
|
5
|
+
|
6
|
+
module LoggableActivity
|
7
|
+
class Activity < ActiveRecord::Base
|
8
|
+
self.table_name = 'loggable_activities'
|
9
|
+
has_many :payloads, class_name: 'LoggableActivity::Payload', dependent: :destroy
|
10
|
+
accepts_nested_attributes_for :payloads
|
11
|
+
|
12
|
+
validates :actor, presence: true
|
13
|
+
validates :action, presence: true
|
14
|
+
# validates :encrypted_record_display_name, presence: true
|
15
|
+
# validates :encrypted_actor_display_name, presence: true
|
16
|
+
|
17
|
+
validate :must_have_at_least_one_payload
|
18
|
+
|
19
|
+
belongs_to :record, polymorphic: true, optional: true
|
20
|
+
belongs_to :actor, polymorphic: true, optional: true
|
21
|
+
|
22
|
+
def attrs
|
23
|
+
# @attrs ||= payloads_attrs
|
24
|
+
payloads_attrs
|
25
|
+
end
|
26
|
+
|
27
|
+
def update_activity_attrs
|
28
|
+
{
|
29
|
+
update_attrs:,
|
30
|
+
updated_relations_attrs:
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def primary_payload_attrs
|
35
|
+
primary_payload ? primary_payload.attrs : {}
|
36
|
+
end
|
37
|
+
|
38
|
+
def primary_payload
|
39
|
+
# @primary_payload ||= ordered_payloads.find { |p| p.payload_type == 'primary_payload' }
|
40
|
+
ordered_payloads.find { |p| p.payload_type == 'primary_payload' }
|
41
|
+
end
|
42
|
+
|
43
|
+
def ordered_payloads
|
44
|
+
# @ordered_payloads ||= payloads.order(:payload_type)
|
45
|
+
payloads.order(:payload_type)
|
46
|
+
end
|
47
|
+
|
48
|
+
def relations_attrs
|
49
|
+
attrs.filter { |p| p[:payload_type] == 'current_association' }
|
50
|
+
end
|
51
|
+
|
52
|
+
def updated_relations_attrs
|
53
|
+
grouped_associations = attrs.group_by { |p| p[:record_class] }
|
54
|
+
|
55
|
+
grouped_associations.map do |record_class, payloads|
|
56
|
+
previous_attrs = payloads.find { |p| p[:payload_type] == 'previous_association' }
|
57
|
+
current_attrs = payloads.find { |p| p[:payload_type] == 'current_association' }
|
58
|
+
next if previous_attrs.nil? && current_attrs.nil?
|
59
|
+
|
60
|
+
{ record_class:, previous_attrs:, current_attrs: }
|
61
|
+
end.compact
|
62
|
+
end
|
63
|
+
|
64
|
+
def payloads_attrs
|
65
|
+
ordered_payloads.map do |payload|
|
66
|
+
{
|
67
|
+
record_class: payload.record_type,
|
68
|
+
payload_type: payload.payload_type,
|
69
|
+
attrs: payload.attrs
|
70
|
+
}
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def update_attrs
|
75
|
+
update_payload_attrs = attrs.find { |p| p[:payload_type] == 'update_payload' }
|
76
|
+
return nil unless update_payload_attrs
|
77
|
+
|
78
|
+
update_payload_attrs.delete(:payload_type)
|
79
|
+
update_payload_attrs
|
80
|
+
end
|
81
|
+
|
82
|
+
def previous_associations_attrs
|
83
|
+
attrs.select { |p| p[:payload_type] == 'previous_association' }
|
84
|
+
end
|
85
|
+
|
86
|
+
def record_display_name
|
87
|
+
return I18n.t('loggable.activity.deleted') if encrypted_record_display_name.nil?
|
88
|
+
|
89
|
+
LoggableActivity::Encryption.decrypt(encrypted_record_display_name, record_key)
|
90
|
+
end
|
91
|
+
|
92
|
+
def actor_display_name
|
93
|
+
return I18n.t('loggable.activity.deleted') if encrypted_actor_display_name.nil?
|
94
|
+
|
95
|
+
LoggableActivity::Encryption.decrypt(encrypted_actor_display_name, actor_key)
|
96
|
+
end
|
97
|
+
|
98
|
+
def actor_key
|
99
|
+
LoggableActivity::EncryptionKey.for_record(actor)&.key
|
100
|
+
end
|
101
|
+
|
102
|
+
def record_key
|
103
|
+
LoggableActivity::EncryptionKey.for_record(record)&.key
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.activities_for_actor(actor)
|
107
|
+
LoggableActivity::Activity.where(actor:).order(created_at: :desc)
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.latest(limit = 20, params = { offset: 0 })
|
111
|
+
offset = params[:offset] || 0
|
112
|
+
LoggableActivity::Activity
|
113
|
+
.all
|
114
|
+
.order(created_at: :desc)
|
115
|
+
.includes(:payloads)
|
116
|
+
.offset(offset)
|
117
|
+
.limit(limit)
|
118
|
+
end
|
119
|
+
|
120
|
+
def must_have_at_least_one_payload
|
121
|
+
errors.add(:payloads, 'must have at least one payload') if payloads.empty?
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LoggableActivity
|
4
|
+
class Configuration
|
5
|
+
def self.load_config_file(config_file_path)
|
6
|
+
@config_data = YAML.load_file(config_file_path)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.for_class(class_name)
|
10
|
+
@config_data[class_name]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This is a module for encryption and decryption of attributes
|
4
|
+
require 'openssl'
|
5
|
+
require 'base64'
|
6
|
+
|
7
|
+
module LoggableActivity
|
8
|
+
class EncryptionError < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
module Encryption
|
12
|
+
def self.encrypt(data, encryption_key)
|
13
|
+
return nil if data.nil?
|
14
|
+
return nil if encryption_key.nil?
|
15
|
+
raise EncryptionError, 'Encryption failed: Invalid encryption key length' unless encryption_key.bytesize == 32
|
16
|
+
|
17
|
+
cipher = OpenSSL::Cipher.new('AES-128-CBC').encrypt
|
18
|
+
cipher.key = Digest::SHA1.hexdigest(encryption_key)[0..15]
|
19
|
+
encrypted = cipher.update(data.to_s) + cipher.final
|
20
|
+
Base64.encode64(encrypted)
|
21
|
+
rescue OpenSSL::Cipher::CipherError => e
|
22
|
+
raise EncryptionError, "Encryption failed: #{e.message} ***"
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.decrypt(data, encryption_key)
|
26
|
+
return I18n.t('loggable.activity.deleted') if blank?(data) || blank?(encryption_key)
|
27
|
+
|
28
|
+
cipher = OpenSSL::Cipher.new('AES-128-CBC').decrypt
|
29
|
+
cipher.key = Digest::SHA1.hexdigest(encryption_key)[0..15]
|
30
|
+
decrypted_data = Base64.decode64(data)
|
31
|
+
decrypted_output = cipher.update(decrypted_data) + cipher.final
|
32
|
+
raise 'Decryption failed: Invalid UTF-8 output' unless decrypted_output.valid_encoding?
|
33
|
+
|
34
|
+
decrypted_output.force_encoding('UTF-8')
|
35
|
+
rescue OpenSSL::Cipher::CipherError => e
|
36
|
+
raise EncryptionError, e.message
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.blank?(value)
|
40
|
+
value.respond_to?(:empty?) ? value.empty? : !value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This the key used to unlock the data for one payload.
|
4
|
+
# When deleted only the encryption_key field is deleted.
|
5
|
+
|
6
|
+
module LoggableActivity
|
7
|
+
class EncryptionKey < ActiveRecord::Base
|
8
|
+
self.table_name = 'loggable_encryption_keys'
|
9
|
+
require 'securerandom'
|
10
|
+
belongs_to :record, polymorphic: true, optional: true
|
11
|
+
belongs_to :parrent_key, class_name: 'LoggableActivity::EncryptionKey', optional: true,
|
12
|
+
foreign_key: 'parrent_key_id'
|
13
|
+
|
14
|
+
def mark_as_deleted
|
15
|
+
update(key: nil)
|
16
|
+
parrent_key.mark_as_deleted if parrent_key.present?
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.for_record_by_type_and_id(record_type, record_id, parrent_key = nil)
|
20
|
+
enctyption_key = find_by(record_type:, record_id:)
|
21
|
+
|
22
|
+
return enctyption_key if enctyption_key
|
23
|
+
|
24
|
+
create_encryption_key(record_type, record_id, parrent_key)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.for_record(record, parrent_key = nil)
|
28
|
+
encryption_key = find_by(record:)
|
29
|
+
return encryption_key if encryption_key
|
30
|
+
|
31
|
+
create_encryption_key(record.class.name, record.id, parrent_key)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.create_encryption_key(record_type, record_id, parrent_key = nil)
|
35
|
+
if parrent_key
|
36
|
+
create(record_type:, record_id:, key: random_key, parrent_key_id: parrent_key.id)
|
37
|
+
else
|
38
|
+
create(record_type:, record_id:, key: random_key)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.random_key
|
43
|
+
SecureRandom.hex(16)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This is the main module for loggable.
|
4
|
+
# When included to a model, it provides the features for creating the activities.
|
5
|
+
require 'loggable_activity/payloads_builder'
|
6
|
+
require 'loggable_activity/update_payloads_builder'
|
7
|
+
|
8
|
+
module LoggableActivity
|
9
|
+
module Hooks
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
include LoggableActivity::PayloadsBuilder
|
12
|
+
include LoggableActivity::UpdatePayloadsBuilder
|
13
|
+
|
14
|
+
included do
|
15
|
+
config = LoggableActivity::Configuration.for_class(name)
|
16
|
+
if config.nil?
|
17
|
+
raise "Loggable::Configuration not found for #{name}, Please add it to 'config/loggable_activity.yaml'"
|
18
|
+
end
|
19
|
+
|
20
|
+
self.loggable_attrs = config&.fetch('loggable_attrs', []) || []
|
21
|
+
self.relations = config&.fetch('relations', []) || []
|
22
|
+
self.auto_log = config&.fetch('auto_log', []) || []
|
23
|
+
self.actor_display_name = config&.fetch('actor_display_name', nil)
|
24
|
+
self.record_display_name = config&.fetch('record_display_name', nil)
|
25
|
+
|
26
|
+
after_create :log_create_activity
|
27
|
+
after_update :log_update_activity
|
28
|
+
before_destroy :log_destroy_activity
|
29
|
+
|
30
|
+
# has_one: encryption_key, as: :encryption_key, class_name: 'LoggableActivity::EncryptionKey'
|
31
|
+
end
|
32
|
+
|
33
|
+
# This is the main method for logging activities.
|
34
|
+
# It is never called from the directly from the controller.
|
35
|
+
def log(action, actor: nil, params: {})
|
36
|
+
@action = action
|
37
|
+
@actor = actor || Thread.current[:current_user]
|
38
|
+
# LoggableActivity::EncryptionKey.for_record(self)
|
39
|
+
|
40
|
+
return if @actor.nil?
|
41
|
+
|
42
|
+
@record = self
|
43
|
+
@params = params
|
44
|
+
@payloads = []
|
45
|
+
|
46
|
+
case action
|
47
|
+
when :create, :show
|
48
|
+
log_activity
|
49
|
+
when :destroy
|
50
|
+
log_destroy
|
51
|
+
when :update
|
52
|
+
log_update
|
53
|
+
else
|
54
|
+
log_custom_activity(action)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def log_activity
|
61
|
+
create_activity(build_payloads)
|
62
|
+
end
|
63
|
+
|
64
|
+
def log_update
|
65
|
+
create_activity(build_update_payloads)
|
66
|
+
end
|
67
|
+
|
68
|
+
def log_destroy
|
69
|
+
create_activity(build_destroy_payload)
|
70
|
+
end
|
71
|
+
|
72
|
+
def create_activity(payloads)
|
73
|
+
return if nothing_to_log?(payloads)
|
74
|
+
|
75
|
+
LoggableActivity::Activity.create!(
|
76
|
+
encrypted_actor_display_name: encrypted_actor_name,
|
77
|
+
encrypted_record_display_name: encrypted_record_name,
|
78
|
+
action: action_key,
|
79
|
+
actor: @actor,
|
80
|
+
record: @record,
|
81
|
+
payloads:
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
def nothing_to_log?(payloads)
|
86
|
+
payloads.empty?
|
87
|
+
end
|
88
|
+
|
89
|
+
def log_custom_activity(activity); end
|
90
|
+
|
91
|
+
def log_update_activity
|
92
|
+
log(:update) if self.class.auto_log.include?('update')
|
93
|
+
end
|
94
|
+
|
95
|
+
def log_create_activity
|
96
|
+
log(:create) if self.class.auto_log.include?('create')
|
97
|
+
end
|
98
|
+
|
99
|
+
def log_destroy_activity
|
100
|
+
LoggableActivity::EncryptionKey.for_record(self)&.mark_as_deleted
|
101
|
+
log(:destroy) if self.class.auto_log.include?('destroy')
|
102
|
+
end
|
103
|
+
|
104
|
+
def encrypted_actor_name
|
105
|
+
actor_display_name = @actor.send(actor_display_name_field)
|
106
|
+
LoggableActivity::Encryption.encrypt(actor_display_name, actor_encryption_key)
|
107
|
+
end
|
108
|
+
|
109
|
+
def encrypted_record_name
|
110
|
+
display_name =
|
111
|
+
if self.class.record_display_name.nil?
|
112
|
+
"#{self.class.name} id: #{id}"
|
113
|
+
else
|
114
|
+
send(self.class.record_display_name.to_sym)
|
115
|
+
end
|
116
|
+
LoggableActivity::Encryption.encrypt(display_name, primary_encryption_key)
|
117
|
+
end
|
118
|
+
|
119
|
+
def action_key
|
120
|
+
@action_key ||= self.class.base_action + ".#{@action}"
|
121
|
+
end
|
122
|
+
|
123
|
+
def primary_encryption_key
|
124
|
+
@primary_encryption_key ||=
|
125
|
+
LoggableActivity::EncryptionKey.for_record(self)&.key
|
126
|
+
end
|
127
|
+
|
128
|
+
def primary_encryption_key_deleted?
|
129
|
+
primary_encryption_key.nil?
|
130
|
+
end
|
131
|
+
|
132
|
+
def actor_encryption_key
|
133
|
+
LoggableActivity::EncryptionKey.for_record(@actor)&.key
|
134
|
+
end
|
135
|
+
|
136
|
+
def actor_display_name_field
|
137
|
+
Rails.application.config.loggable_activity.actor_display_name || "id: #{@actor.id}, class: #{@actor.class.name}"
|
138
|
+
end
|
139
|
+
|
140
|
+
def current_user_model?
|
141
|
+
Rails.application.config.loggable_activity.current_user_model_name == self.class.name
|
142
|
+
end
|
143
|
+
|
144
|
+
class_methods do
|
145
|
+
attr_accessor :loggable_attrs, :relations, :auto_log, :actor_display_name, :record_display_name
|
146
|
+
|
147
|
+
def base_action
|
148
|
+
name.downcase.gsub('::', '/')
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This is the payload of the log. It contains the encrypted data of one record in the DB.
|
4
|
+
# When the record is deleted, the encryption_key for the payload is deleted.
|
5
|
+
# Payloads comes in different flavors.
|
6
|
+
# The primary_payload is the payload that contains the parrent record.
|
7
|
+
# When fecthing the attrs, they are packed differently depending on the payload_type.
|
8
|
+
|
9
|
+
module LoggableActivity
|
10
|
+
class Payload < ActiveRecord::Base
|
11
|
+
self.table_name = 'loggable_payloads'
|
12
|
+
belongs_to :record, polymorphic: true, optional: true
|
13
|
+
|
14
|
+
belongs_to :activity
|
15
|
+
belongs_to :record, polymorphic: true, optional: true
|
16
|
+
validates :encrypted_attrs, presence: true
|
17
|
+
enum payload_type: {
|
18
|
+
primary_payload: 0,
|
19
|
+
update_payload: 1,
|
20
|
+
current_association: 2,
|
21
|
+
previous_association: 3
|
22
|
+
}
|
23
|
+
|
24
|
+
def attrs
|
25
|
+
return deleted_attrs if record.nil?
|
26
|
+
|
27
|
+
case payload_type
|
28
|
+
when 'current_association', 'primary_payload', 'previous_association'
|
29
|
+
decrypted_attrs
|
30
|
+
when 'update_payload'
|
31
|
+
decrypted_update_attrs
|
32
|
+
# when 'destroy_payload'
|
33
|
+
# destroy_payload_attrs
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def payload_encryption_key
|
38
|
+
@payload_encryption_key ||= LoggableActivity::EncryptionKey.for_record(record)&.key
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def deleted_attrs
|
44
|
+
encrypted_attrs.transform_values! { I18n.t('loggable.activity.deleted') }
|
45
|
+
end
|
46
|
+
|
47
|
+
def decrypted_update_attrs
|
48
|
+
encrypted_attrs['changes'].map do |change|
|
49
|
+
decrypted_from_to_attr(change)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def decrypted_from_to_attr(change)
|
54
|
+
change.to_h do |key, value|
|
55
|
+
from = decrypt_attr(value['from'])
|
56
|
+
to = decrypt_attr(value['to'])
|
57
|
+
[key, { from:, to: }]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def decrypted_attrs
|
62
|
+
encrypted_attrs.each do |key, value|
|
63
|
+
encrypted_attrs[key] = decrypt_attr(value)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def decrypt_attr(value)
|
68
|
+
LoggableActivity::Encryption.decrypt(value, payload_encryption_key)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This is a factory for building payloads.
|
4
|
+
|
5
|
+
module LoggableActivity
|
6
|
+
module PayloadsBuilder
|
7
|
+
def build_payloads
|
8
|
+
build_primary_payload
|
9
|
+
self.class.relations.each do |relation_config|
|
10
|
+
build_relation_payload(relation_config)
|
11
|
+
end
|
12
|
+
@payloads
|
13
|
+
end
|
14
|
+
|
15
|
+
def build_primary_payload
|
16
|
+
encrypted_attrs = encrypt_attrs(attributes, self.class.loggable_attrs, primary_encryption_key)
|
17
|
+
@payloads << LoggableActivity::Payload.new(
|
18
|
+
record: @record,
|
19
|
+
payload_type: 'primary_payload',
|
20
|
+
encrypted_attrs:,
|
21
|
+
data_owner: true
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def build_destroy_payload
|
26
|
+
encrypted_attrs = encrypt_attrs(attributes, self.class.loggable_attrs, primary_encryption_key)
|
27
|
+
encrypted_attrs.transform_values! { '*** DELETED ***' }
|
28
|
+
@payloads << LoggableActivity::Payload.new(
|
29
|
+
record: @record,
|
30
|
+
payload_type: 'primary_payload',
|
31
|
+
encrypted_attrs:,
|
32
|
+
data_owner: true
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
def build_relation_payload(relation_config)
|
37
|
+
relation_config.each_key do |key|
|
38
|
+
case key
|
39
|
+
when 'belongs_to'
|
40
|
+
build_payload(relation_config, 'belongs_to')
|
41
|
+
when 'has_one'
|
42
|
+
build_payload(relation_config, 'has_one')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def build_payload(relation_config, ralation_type)
|
48
|
+
associated_record = send(relation_config[ralation_type])
|
49
|
+
return nil if associated_record.nil?
|
50
|
+
|
51
|
+
associated_loggable_attrs = relation_config['loggable_attrs']
|
52
|
+
|
53
|
+
encryption_key = associated_record_encryption_key(associated_record, relation_config['data_owner'])
|
54
|
+
|
55
|
+
encrypted_attrs =
|
56
|
+
encrypt_attrs(
|
57
|
+
associated_record.attributes,
|
58
|
+
associated_loggable_attrs,
|
59
|
+
encryption_key.key
|
60
|
+
)
|
61
|
+
|
62
|
+
@payloads << LoggableActivity::Payload.new(
|
63
|
+
record: associated_record,
|
64
|
+
encrypted_attrs:,
|
65
|
+
payload_type: 'current_association',
|
66
|
+
data_owner: relation_config['data_owner']
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
def associated_record_encryption_key(associated_record, data_owner)
|
71
|
+
if data_owner
|
72
|
+
LoggableActivity::EncryptionKey.for_record(associated_record, LoggableActivity::EncryptionKey.for_record(self))
|
73
|
+
else
|
74
|
+
LoggableActivity::EncryptionKey.for_record(associated_record)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def encrypt_attrs(attrs, loggable_attrs, encryption_key)
|
79
|
+
attrs = attrs.slice(*loggable_attrs)
|
80
|
+
encrypt_attr(attrs, encryption_key)
|
81
|
+
end
|
82
|
+
|
83
|
+
def encrypt_attr(attrs, encryption_key)
|
84
|
+
attrs.each do |key, value|
|
85
|
+
attrs[key] = LoggableActivity::Encryption.encrypt(value, encryption_key)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This is a factory for building update payloads
|
4
|
+
|
5
|
+
module LoggableActivity
|
6
|
+
module UpdatePayloadsBuilder
|
7
|
+
def build_update_payloads
|
8
|
+
@update_payloads = []
|
9
|
+
|
10
|
+
previous_values, current_values = primary_update_attrs
|
11
|
+
build_primary_update_payload(previous_values, current_values)
|
12
|
+
|
13
|
+
self.class.relations.each do |relation_config|
|
14
|
+
build_update_relation_payloads(relation_config)
|
15
|
+
end
|
16
|
+
@update_payloads
|
17
|
+
end
|
18
|
+
|
19
|
+
def primary_update_attrs
|
20
|
+
previous_values = saved_changes.transform_values(&:first)
|
21
|
+
current_values = saved_changes.transform_values(&:last)
|
22
|
+
|
23
|
+
[previous_values, current_values]
|
24
|
+
end
|
25
|
+
|
26
|
+
def build_primary_update_payload(previous_values, current_values)
|
27
|
+
return if previous_values == current_values
|
28
|
+
|
29
|
+
encrypted_update_attrs = encrypted_update_attrs(previous_values, current_values)
|
30
|
+
@update_payloads << LoggableActivity::Payload.new(
|
31
|
+
record: @record,
|
32
|
+
payload_type: 'update_payload',
|
33
|
+
encrypted_attrs: encrypted_update_attrs
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
def encrypted_update_attrs(previous_values, current_values)
|
38
|
+
changes = []
|
39
|
+
changed_attrs = previous_values.slice(*self.class.loggable_attrs)
|
40
|
+
changed_attrs.each do |key, from_value|
|
41
|
+
from = LoggableActivity::Encryption.encrypt(from_value, primary_encryption_key)
|
42
|
+
to_value = current_values[key]
|
43
|
+
to = LoggableActivity::Encryption.encrypt(to_value, primary_encryption_key)
|
44
|
+
changes << { key => { from:, to: } }
|
45
|
+
end
|
46
|
+
{ changes: }
|
47
|
+
end
|
48
|
+
|
49
|
+
def build_update_relation_payloads(relation_config)
|
50
|
+
relation_config.each_key do |key|
|
51
|
+
case key
|
52
|
+
when 'belongs_to'
|
53
|
+
build_relation_update_for_belongs_to(relation_config)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def build_relation_update_for_belongs_to(relation_config)
|
59
|
+
relation_id = "#{relation_config['belongs_to']}_id"
|
60
|
+
model_class_name = relation_config['model']
|
61
|
+
model_class = model_class_name.constantize
|
62
|
+
|
63
|
+
return unless saved_changes.include?(relation_id)
|
64
|
+
|
65
|
+
relation_id_changes = saved_changes[relation_id]
|
66
|
+
previous_relation_id, current_relation_id = relation_id_changes
|
67
|
+
|
68
|
+
[previous_relation_id, current_relation_id].each_with_index do |id, index|
|
69
|
+
relation_record = id ? model_class.find_by(id:) : nil
|
70
|
+
next unless relation_record
|
71
|
+
|
72
|
+
payload_type = index.zero? ? 'previous_association' : 'current_association'
|
73
|
+
build_relation_update_payload(
|
74
|
+
relation_record.attributes,
|
75
|
+
relation_config['loggable_attrs'],
|
76
|
+
relation_record,
|
77
|
+
payload_type
|
78
|
+
)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def build_relation_update_payload(_attrs, loggable_attrs, record, payload_type)
|
83
|
+
encryption_key = LoggableActivity::EncryptionKey.for_record(record)&.key
|
84
|
+
encrypted_attrs = relation_encrypted_attrs(record.attributes, loggable_attrs, encryption_key)
|
85
|
+
|
86
|
+
ap "building relation update payload for #{record.id} with encryption_key: #{encryption_key}"
|
87
|
+
|
88
|
+
@update_payloads << LoggableActivity::Payload.new(
|
89
|
+
record:,
|
90
|
+
encrypted_attrs:,
|
91
|
+
payload_type:
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
def relation_encrypted_attrs(attrs, loggable_attrs, encryption_key)
|
96
|
+
encrypt_attrs(attrs, loggable_attrs, encryption_key)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# require 'rails'
|
4
|
+
|
5
|
+
require_relative 'loggable_activity/activity'
|
6
|
+
require_relative 'loggable_activity/configuration'
|
7
|
+
require_relative 'loggable_activity/encryption'
|
8
|
+
require_relative 'loggable_activity/encryption_key'
|
9
|
+
require_relative 'loggable_activity/hooks'
|
10
|
+
require_relative 'loggable_activity/payload'
|
11
|
+
require_relative 'loggable_activity/payloads_builder'
|
12
|
+
require_relative 'loggable_activity/update_payloads_builder'
|
13
|
+
require_relative 'loggable_activity/version'
|
14
|
+
|
15
|
+
module LoggableActivity
|
16
|
+
class Error < StandardError; end
|
17
|
+
# Your code goes here...
|
18
|
+
end
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|