loggable_activity 0.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|