lesli_audit 1.1.0 → 1.1.2
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 +4 -4
- data/Rakefile +6 -1
- data/app/assets/stylesheets/lesli_audit/application.css +366 -35
- data/app/controllers/lesli_audit/accounts_controller.rb +7 -3
- data/app/controllers/lesli_audit/logs_controller.rb +3 -8
- data/app/controllers/lesli_audit/visitors_controller.rb +1 -1
- data/app/interfaces/lesli_audit/logger_interface.rb +56 -59
- data/app/jobs/lesli_audit/retention_cleanup_job.rb +9 -0
- data/app/models/concerns/lesli_audit/append_only.rb +12 -0
- data/app/models/lesli_audit/account.rb +26 -10
- data/app/models/lesli_audit/account_device.rb +3 -1
- data/app/models/lesli_audit/account_log.rb +12 -7
- data/app/models/lesli_audit/account_request.rb +3 -1
- data/app/models/lesli_audit/user_journal.rb +16 -3
- data/app/models/lesli_audit/user_log.rb +12 -3
- data/app/models/lesli_audit/user_request.rb +7 -3
- data/app/services/lesli_audit/activity_audit.rb +130 -0
- data/app/services/lesli_audit/request_service.rb +2 -35
- data/app/services/lesli_audit/retention_cleanup.rb +146 -0
- data/app/services/lesli_audit/user_agent_parser.rb +51 -0
- data/app/services/lesli_audit/user_service.rb +18 -16
- data/app/services/lesli_audit/visitor_service.rb +22 -49
- data/app/views/lesli_audit/logs/show.html.erb +14 -8
- data/app/views/lesli_audit/partials/_navigation.html.erb +1 -1
- data/app/views/lesli_audit/users/index.html.erb +67 -64
- data/app/views/lesli_audit/visitors/index.html.erb +126 -80
- data/config/routes.rb +3 -2
- data/db/migrate/v1.0/0501000110_create_lesli_audit_accounts.rb +14 -1
- data/db/migrate/v1.0/0501110110_create_lesli_audit_account_logs.rb +10 -6
- data/db/migrate/v1.0/0501110210_create_lesli_audit_account_devices.rb +10 -7
- data/db/migrate/v1.0/0501110310_create_lesli_audit_account_requests.rb +10 -7
- data/db/migrate/v1.0/0501120110_create_lesli_audit_user_logs.rb +11 -7
- data/db/migrate/v1.0/0501120210_create_lesli_audit_user_journals.rb +19 -8
- data/db/migrate/v1.0/0501120310_create_lesli_audit_user_requests.rb +13 -10
- data/db/seed/devices.rb +1 -1
- data/db/seed/requests.rb +3 -3
- data/lib/lesli_audit/engine.rb +1 -6
- data/lib/lesli_audit/version.rb +2 -2
- data/lib/tasks/lesli_audit_tasks.rake +12 -4
- data/readme.md +127 -50
- metadata +14 -4
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module LesliAudit
|
|
2
|
+
module AppendOnly
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
# New records remain writable until their first INSERT. Once persisted,
|
|
6
|
+
# Active Record blocks ordinary updates and destroys. Retention cleanup
|
|
7
|
+
# and tenant hard-purge foreign keys intentionally operate below models.
|
|
8
|
+
def readonly?
|
|
9
|
+
persisted? || super
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -1,19 +1,35 @@
|
|
|
1
1
|
module LesliAudit
|
|
2
2
|
class Account < ApplicationRecord
|
|
3
|
-
belongs_to :account,
|
|
3
|
+
belongs_to :account,
|
|
4
|
+
class_name: "Lesli::Account",
|
|
5
|
+
inverse_of: :audit
|
|
6
|
+
|
|
7
|
+
validates :account_id, uniqueness: true
|
|
8
|
+
|
|
4
9
|
has_many :dashboards
|
|
5
10
|
|
|
6
|
-
has_many :account_logs
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
has_many :account_logs,
|
|
12
|
+
inverse_of: :account,
|
|
13
|
+
dependent: :delete_all
|
|
14
|
+
|
|
15
|
+
has_many :account_devices,
|
|
16
|
+
inverse_of: :account,
|
|
17
|
+
dependent: :delete_all
|
|
18
|
+
|
|
19
|
+
has_many :account_requests,
|
|
20
|
+
inverse_of: :account,
|
|
21
|
+
dependent: :delete_all
|
|
9
22
|
|
|
10
|
-
has_many :user_logs
|
|
11
|
-
|
|
12
|
-
|
|
23
|
+
has_many :user_logs,
|
|
24
|
+
inverse_of: :account,
|
|
25
|
+
dependent: :delete_all
|
|
13
26
|
|
|
14
|
-
|
|
27
|
+
has_many :user_journals,
|
|
28
|
+
inverse_of: :account,
|
|
29
|
+
dependent: :delete_all
|
|
15
30
|
|
|
16
|
-
|
|
17
|
-
|
|
31
|
+
has_many :user_requests,
|
|
32
|
+
inverse_of: :account,
|
|
33
|
+
dependent: :delete_all
|
|
18
34
|
end
|
|
19
35
|
end
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
module LesliAudit
|
|
2
2
|
class AccountLog < ApplicationRecord
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
:
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
include LesliAudit::AppendOnly
|
|
4
|
+
|
|
5
|
+
belongs_to :account,
|
|
6
|
+
class_name: "LesliAudit::Account",
|
|
7
|
+
inverse_of: :account_logs
|
|
8
|
+
|
|
9
|
+
belongs_to :user,
|
|
10
|
+
class_name: "Lesli::User",
|
|
11
|
+
inverse_of: :account_logs,
|
|
12
|
+
optional: true
|
|
13
|
+
|
|
14
|
+
validates :operation, presence: true
|
|
10
15
|
end
|
|
11
16
|
end
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
module LesliAudit
|
|
2
2
|
class UserJournal < ApplicationRecord
|
|
3
|
-
belongs_to :account
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
belongs_to :account,
|
|
4
|
+
class_name: "LesliAudit::Account",
|
|
5
|
+
inverse_of: :user_journals
|
|
6
|
+
|
|
7
|
+
belongs_to :user,
|
|
8
|
+
class_name: "Lesli::User",
|
|
9
|
+
inverse_of: :journals,
|
|
10
|
+
optional: true
|
|
11
|
+
|
|
12
|
+
validates :event_uid, presence: true
|
|
13
|
+
validates :request_controller, :request_action, presence: true, if: :retain?
|
|
14
|
+
|
|
15
|
+
scope :pending, -> { where(processed_at: nil) }
|
|
16
|
+
scope :processed, -> { where.not(processed_at: nil) }
|
|
17
|
+
scope :retained, -> { where(retain: true) }
|
|
18
|
+
scope :transient, -> { where(retain: false) }
|
|
6
19
|
end
|
|
7
20
|
end
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
module LesliAudit
|
|
2
2
|
class UserLog < ApplicationRecord
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
belongs_to :
|
|
3
|
+
include LesliAudit::AppendOnly
|
|
4
|
+
|
|
5
|
+
belongs_to :account,
|
|
6
|
+
class_name: "LesliAudit::Account",
|
|
7
|
+
inverse_of: :user_logs
|
|
8
|
+
|
|
9
|
+
belongs_to :user,
|
|
10
|
+
class_name: "Lesli::User",
|
|
11
|
+
inverse_of: :logs,
|
|
12
|
+
optional: true
|
|
13
|
+
|
|
14
|
+
validates :operation, presence: true
|
|
6
15
|
end
|
|
7
16
|
end
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
module LesliAudit
|
|
2
2
|
class UserRequest < ApplicationRecord
|
|
3
|
-
belongs_to :account
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
belongs_to :account,
|
|
4
|
+
class_name: "LesliAudit::Account",
|
|
5
|
+
inverse_of: :user_requests
|
|
6
|
+
|
|
7
|
+
belongs_to :user,
|
|
8
|
+
class_name: "Lesli::User",
|
|
9
|
+
inverse_of: :requests
|
|
6
10
|
end
|
|
7
11
|
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
=begin
|
|
2
|
+
|
|
3
|
+
Lesli
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2026, Lesli Technologies, S. A.
|
|
6
|
+
|
|
7
|
+
This program is free software: you can redistribute it and/or modify
|
|
8
|
+
it under the terms of the GNU General Public License as published by
|
|
9
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
(at your option) any later version.
|
|
11
|
+
|
|
12
|
+
This program is distributed in the hope that it will be useful,
|
|
13
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
GNU General Public License for more details.
|
|
16
|
+
|
|
17
|
+
You should have received a copy of the GNU General Public License
|
|
18
|
+
along with this program. If not, see http://www.gnu.org/licenses/.
|
|
19
|
+
|
|
20
|
+
Lesli · Ruby on Rails SaaS Development Framework.
|
|
21
|
+
|
|
22
|
+
Made with ♥ by LesliTech
|
|
23
|
+
Building a better future, one line of code at a time.
|
|
24
|
+
|
|
25
|
+
@contact hello@lesli.tech
|
|
26
|
+
@website https://www.lesli.tech
|
|
27
|
+
@license GPLv3 http://www.gnu.org/licenses/gpl-3.0.en.html
|
|
28
|
+
|
|
29
|
+
// · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
|
|
30
|
+
// ·
|
|
31
|
+
=end
|
|
32
|
+
|
|
33
|
+
module LesliAudit
|
|
34
|
+
# Writes durable business and security events. Unlike request analytics,
|
|
35
|
+
# this entry point never rescues persistence errors: an auditable operation
|
|
36
|
+
# must fail when its event cannot be stored.
|
|
37
|
+
class ActivityAudit
|
|
38
|
+
class MissingAccount < StandardError; end
|
|
39
|
+
|
|
40
|
+
def self.record_account!(account:, actor: nil, **attributes)
|
|
41
|
+
return unless enabled?
|
|
42
|
+
|
|
43
|
+
new(account: account, actor: actor, **attributes).record_account!
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.record_user!(user:, **attributes)
|
|
47
|
+
return unless enabled?
|
|
48
|
+
|
|
49
|
+
new(account: user&.account, actor: user, **attributes).record_user!
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.enabled?
|
|
53
|
+
Lesli.config.audit&.dig(:enable_logs) == true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def initialize(
|
|
57
|
+
account:,
|
|
58
|
+
actor: nil,
|
|
59
|
+
engine: nil,
|
|
60
|
+
source: nil,
|
|
61
|
+
action: nil,
|
|
62
|
+
operation: nil,
|
|
63
|
+
description: nil,
|
|
64
|
+
session_id: nil,
|
|
65
|
+
subject: nil
|
|
66
|
+
)
|
|
67
|
+
@audit_account = resolve_audit_account(account)
|
|
68
|
+
@actor = actor
|
|
69
|
+
@engine = identifier(engine)
|
|
70
|
+
@source = identifier(source)
|
|
71
|
+
@action = identifier(action)
|
|
72
|
+
@operation = identifier(operation.presence || action.presence)
|
|
73
|
+
@description = description
|
|
74
|
+
@session_id = session_id.presence
|
|
75
|
+
@subject = subject
|
|
76
|
+
|
|
77
|
+
raise ArgumentError, "operation is required" if @operation.blank?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def record_account!
|
|
81
|
+
audit_account.account_logs.create!(event_attributes.merge(user: actor))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def record_user!
|
|
85
|
+
raise ArgumentError, "user is required" unless actor
|
|
86
|
+
|
|
87
|
+
audit_account.user_logs.create!(event_attributes.merge(user: actor))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
attr_reader :actor, :audit_account, :subject
|
|
93
|
+
|
|
94
|
+
def resolve_audit_account(account)
|
|
95
|
+
resolved = account if account.is_a?(LesliAudit::Account)
|
|
96
|
+
resolved ||= account&.audit if account&.respond_to?(:audit)
|
|
97
|
+
|
|
98
|
+
return resolved if resolved
|
|
99
|
+
|
|
100
|
+
raise MissingAccount, "LesliAudit account is required"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Keep event metadata deliberately small. Request parameters and other
|
|
104
|
+
# unfiltered payloads are not accepted by this API.
|
|
105
|
+
def event_attributes
|
|
106
|
+
{
|
|
107
|
+
engine: @engine,
|
|
108
|
+
source: @source,
|
|
109
|
+
action: @action,
|
|
110
|
+
operation: @operation,
|
|
111
|
+
description: @description,
|
|
112
|
+
session_id: @session_id,
|
|
113
|
+
actor_uid: actor_uid,
|
|
114
|
+
subject_type: subject&.class&.name,
|
|
115
|
+
subject_id: subject&.id
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def actor_uid
|
|
120
|
+
actor.uid if actor&.respond_to?(:uid)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def identifier(value)
|
|
124
|
+
return if value.nil?
|
|
125
|
+
return value.name if value.respond_to?(:name) && value.name.present?
|
|
126
|
+
|
|
127
|
+
value.to_s
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -38,44 +38,11 @@ module LesliAudit
|
|
|
38
38
|
# @param {query} Has of the formated queries/filters that will be applied to filter data
|
|
39
39
|
# @description
|
|
40
40
|
# @example
|
|
41
|
-
def index
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
requests = current_user.requests
|
|
41
|
+
def index
|
|
42
|
+
current_user.requests
|
|
45
43
|
.order(created_at: :desc)
|
|
46
44
|
.page(query[:pagination][:page])
|
|
47
45
|
.per(query[:pagination][:perPage])
|
|
48
|
-
|
|
49
|
-
return requests
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
search_string = nil # query[:search].downcase.gsub(" ","%") unless query[:search].blank?
|
|
53
|
-
order_string = "user_requests.created_at"
|
|
54
|
-
|
|
55
|
-
user_requests = current_user.account.users.joins(:requests)
|
|
56
|
-
#user_requests = user_requests.where("users.email = ?", query[:filters][:user]) unless query[:filters][:user].blank?
|
|
57
|
-
|
|
58
|
-
if !search_string.blank?
|
|
59
|
-
# user_requests = user_requests.where([
|
|
60
|
-
# "lower(users.email) like :search_string",
|
|
61
|
-
# "lower(request_controller) like :search_string",
|
|
62
|
-
# "lower(request_action) like :search_string"
|
|
63
|
-
# ].join(" or "), {
|
|
64
|
-
# search_string: "%#{search_string}%"
|
|
65
|
-
# })
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
user_requests.select(
|
|
69
|
-
:id,
|
|
70
|
-
"users.id as user_id",
|
|
71
|
-
"users.email",
|
|
72
|
-
:request_controller,
|
|
73
|
-
:request_action,
|
|
74
|
-
:request_method,
|
|
75
|
-
:user_session_id,
|
|
76
|
-
Date2.new.date_time.db_timestamps("user_requests")
|
|
77
|
-
).page(query[:pagination][:page])
|
|
78
|
-
.per(query[:pagination][:perPage])
|
|
79
46
|
end
|
|
80
47
|
end
|
|
81
48
|
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
=begin
|
|
2
|
+
|
|
3
|
+
Lesli
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2026, Lesli Technologies, S. A.
|
|
6
|
+
|
|
7
|
+
This program is free software: you can redistribute it and/or modify
|
|
8
|
+
it under the terms of the GNU General Public License as published by
|
|
9
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
(at your option) any later version.
|
|
11
|
+
|
|
12
|
+
This program is distributed in the hope that it will be useful,
|
|
13
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
GNU General Public License for more details.
|
|
16
|
+
|
|
17
|
+
You should have received a copy of the GNU General Public License
|
|
18
|
+
along with this program. If not, see http://www.gnu.org/licenses/.
|
|
19
|
+
|
|
20
|
+
Lesli · Ruby on Rails SaaS Development Framework.
|
|
21
|
+
|
|
22
|
+
Made with ♥ by LesliTech
|
|
23
|
+
Building a better future, one line of code at a time.
|
|
24
|
+
|
|
25
|
+
@contact hello@lesli.tech
|
|
26
|
+
@website https://www.lesli.tech
|
|
27
|
+
@license GPLv3 http://www.gnu.org/licenses/gpl-3.0.en.html
|
|
28
|
+
|
|
29
|
+
// · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
|
|
30
|
+
// ·
|
|
31
|
+
=end
|
|
32
|
+
|
|
33
|
+
module LesliAudit
|
|
34
|
+
# Applies the retention policy configured by the host application.
|
|
35
|
+
#
|
|
36
|
+
# Cleanup uses delete_all deliberately: analytics has no destroy callbacks,
|
|
37
|
+
# while activity events are append-only during their retained lifetime. Each
|
|
38
|
+
# relation is deleted in bounded primary-key batches and can be retried safely.
|
|
39
|
+
class RetentionCleanup
|
|
40
|
+
DEFAULT_BATCH_SIZE = 1_000
|
|
41
|
+
|
|
42
|
+
ANALYTICS_MODELS = [
|
|
43
|
+
LesliAudit::AccountDevice,
|
|
44
|
+
LesliAudit::AccountRequest,
|
|
45
|
+
LesliAudit::UserRequest
|
|
46
|
+
].freeze
|
|
47
|
+
|
|
48
|
+
ACTIVITY_MODELS = [
|
|
49
|
+
LesliAudit::AccountLog,
|
|
50
|
+
LesliAudit::UserLog
|
|
51
|
+
].freeze
|
|
52
|
+
|
|
53
|
+
def self.call(batch_size: DEFAULT_BATCH_SIZE, today: Date.current, now: Time.current)
|
|
54
|
+
new(batch_size: batch_size, today: today, now: now).call
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def initialize(batch_size:, today:, now:)
|
|
58
|
+
@batch_size = positive_integer!(batch_size, :batch_size)
|
|
59
|
+
@today = today.to_date
|
|
60
|
+
@now = now
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def call
|
|
64
|
+
deleted = {}
|
|
65
|
+
|
|
66
|
+
cleanup_journals(deleted)
|
|
67
|
+
cleanup_group(
|
|
68
|
+
deleted,
|
|
69
|
+
ANALYTICS_MODELS,
|
|
70
|
+
column: :date,
|
|
71
|
+
cutoff: date_cutoff(:analytics_retention_days)
|
|
72
|
+
)
|
|
73
|
+
cleanup_group(
|
|
74
|
+
deleted,
|
|
75
|
+
ACTIVITY_MODELS,
|
|
76
|
+
column: :created_at,
|
|
77
|
+
cutoff: time_cutoff(:audit_retention_days)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
deleted
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
attr_reader :batch_size, :now, :today
|
|
86
|
+
|
|
87
|
+
def cleanup_journals(deleted)
|
|
88
|
+
retained_count = delete_with_cutoff(
|
|
89
|
+
LesliAudit::UserJournal.retained.processed,
|
|
90
|
+
:date,
|
|
91
|
+
date_cutoff(:journal_retention_days)
|
|
92
|
+
)
|
|
93
|
+
transient_count = delete_with_cutoff(
|
|
94
|
+
LesliAudit::UserJournal.transient.processed,
|
|
95
|
+
:date,
|
|
96
|
+
date_cutoff(:analytics_retention_days)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
deleted[LesliAudit::UserJournal.table_name] = retained_count + transient_count
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def cleanup_group(deleted, models, column:, cutoff:)
|
|
103
|
+
models.each do |model|
|
|
104
|
+
deleted[model.table_name] = delete_with_cutoff(model.all, column, cutoff)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def delete_with_cutoff(relation, column, cutoff)
|
|
109
|
+
return 0 unless cutoff
|
|
110
|
+
|
|
111
|
+
relation
|
|
112
|
+
.where(relation.klass.arel_table[column].lt(cutoff))
|
|
113
|
+
.in_batches(of: batch_size)
|
|
114
|
+
.delete_all
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def date_cutoff(setting)
|
|
118
|
+
days = retention_days(setting)
|
|
119
|
+
today - days if days
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def time_cutoff(setting)
|
|
123
|
+
days = retention_days(setting)
|
|
124
|
+
now - days.days if days
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def retention_days(setting)
|
|
128
|
+
value = Lesli.config.audit&.dig(setting)
|
|
129
|
+
return if value.nil?
|
|
130
|
+
|
|
131
|
+
positive_integer!(value, setting)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def positive_integer!(value, setting)
|
|
135
|
+
valid_value = value.is_a?(Integer) || (value.is_a?(String) && value.match?(/\A[1-9]\d*\z/))
|
|
136
|
+
raise ArgumentError unless valid_value
|
|
137
|
+
|
|
138
|
+
integer = value.to_i
|
|
139
|
+
return integer if integer.positive?
|
|
140
|
+
|
|
141
|
+
raise ArgumentError, "#{setting} must be a positive integer or nil"
|
|
142
|
+
rescue ArgumentError, TypeError
|
|
143
|
+
raise ArgumentError, "#{setting} must be a positive integer or nil"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
=begin
|
|
2
|
+
|
|
3
|
+
Lesli
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2026, Lesli Technologies, S. A.
|
|
6
|
+
|
|
7
|
+
This program is free software: you can redistribute it and/or modify
|
|
8
|
+
it under the terms of the GNU General Public License as published by
|
|
9
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
(at your option) any later version.
|
|
11
|
+
|
|
12
|
+
This program is distributed in the hope that it will be useful,
|
|
13
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
GNU General Public License for more details.
|
|
16
|
+
|
|
17
|
+
You should have received a copy of the GNU General Public License
|
|
18
|
+
along with this program. If not, see http://www.gnu.org/licenses/.
|
|
19
|
+
|
|
20
|
+
Lesli · Ruby on Rails SaaS Development Framework.
|
|
21
|
+
|
|
22
|
+
Made with ♥ by LesliTech
|
|
23
|
+
Building a better future, one line of code at a time.
|
|
24
|
+
|
|
25
|
+
@contact hello@lesli.tech
|
|
26
|
+
@website https://www.lesli.tech
|
|
27
|
+
@license GPLv3 http://www.gnu.org/licenses/gpl-3.0.en.html
|
|
28
|
+
|
|
29
|
+
// · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
|
|
30
|
+
// ·
|
|
31
|
+
=end
|
|
32
|
+
|
|
33
|
+
require "device_detector"
|
|
34
|
+
|
|
35
|
+
module LesliAudit
|
|
36
|
+
# Provides one normalized user-agent contract for the engines that need to
|
|
37
|
+
# persist browser and operating-system metadata.
|
|
38
|
+
class UserAgentParser
|
|
39
|
+
def self.parse(user_agent)
|
|
40
|
+
detector = DeviceDetector.new(user_agent.to_s)
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
platform: detector.os_name,
|
|
44
|
+
os: detector.os_full_version,
|
|
45
|
+
browser: detector.name,
|
|
46
|
+
version: detector.full_version,
|
|
47
|
+
device: detector.device_type
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -35,22 +35,24 @@ module LesliAudit
|
|
|
35
35
|
|
|
36
36
|
def users
|
|
37
37
|
users = current_user.account.users
|
|
38
|
+
users = users.with_deleted if users.respond_to?(:with_deleted)
|
|
38
39
|
|
|
39
40
|
now = Time.current
|
|
40
41
|
last_30_days = 30.days.ago
|
|
42
|
+
quoted_false = ActiveRecord::Base.connection.quote(false)
|
|
41
43
|
|
|
42
44
|
stats = users.pick(
|
|
43
45
|
# total users
|
|
44
46
|
Arel.sql("COUNT(*)"),
|
|
45
47
|
|
|
46
48
|
# total deactivated users
|
|
47
|
-
Arel.sql("SUM(CASE WHEN active =
|
|
49
|
+
Arel.sql("SUM(CASE WHEN active = #{quoted_false} AND deleted_at IS NULL THEN 1 ELSE 0 END)"),
|
|
48
50
|
|
|
49
51
|
# total locked users
|
|
50
52
|
Arel.sql(
|
|
51
53
|
"SUM(CASE WHEN (
|
|
52
54
|
locked_at IS NOT NULL
|
|
53
|
-
OR locked_until > #{ActiveRecord::Base.connection.quote(
|
|
55
|
+
OR locked_until > #{ActiveRecord::Base.connection.quote(now)}
|
|
54
56
|
) AND deleted_at IS NULL THEN 1 ELSE 0 END)"
|
|
55
57
|
),
|
|
56
58
|
|
|
@@ -137,37 +139,37 @@ module LesliAudit
|
|
|
137
139
|
|
|
138
140
|
group = 'day'
|
|
139
141
|
|
|
140
|
-
group_by = "DATE_TRUNC('month',
|
|
141
|
-
group_by = "DATE_TRUNC('week',
|
|
142
|
-
group_by = "DATE_TRUNC('day',
|
|
142
|
+
group_by = "DATE_TRUNC('month', lesli_audit_user_journals.date)" if group == 'month'
|
|
143
|
+
group_by = "DATE_TRUNC('week', lesli_audit_user_journals.date)" if group == 'week'
|
|
144
|
+
group_by = "DATE_TRUNC('day', lesli_audit_user_journals.date)" if group == 'day'
|
|
143
145
|
|
|
144
146
|
# compatibility for SQLite
|
|
145
147
|
if ActiveRecord::Base.connection.adapter_name == "SQLite"
|
|
146
|
-
group_by = "strftime('%Y-%m-%d',
|
|
148
|
+
group_by = "strftime('%Y-%m-%d', lesli_audit_user_journals.date)"
|
|
147
149
|
end
|
|
148
150
|
|
|
149
|
-
|
|
151
|
+
user_journals = current_user.account.users
|
|
152
|
+
.joins(:journals)
|
|
153
|
+
.merge(LesliAudit::UserJournal.retained.processed)
|
|
150
154
|
|
|
151
|
-
|
|
152
|
-
:id,
|
|
155
|
+
user_journals = user_journals.select(
|
|
153
156
|
"lesli_users.id as user_id",
|
|
154
157
|
"lesli_users.email as user_email",
|
|
155
158
|
"#{group_by} date",
|
|
156
|
-
"min(
|
|
157
|
-
"max(
|
|
158
|
-
"count(
|
|
159
|
-
"sum(lesli_audit_user_requests.request_count) requests"
|
|
159
|
+
"min(lesli_audit_user_journals.created_at) as first_activity",
|
|
160
|
+
"max(lesli_audit_user_journals.created_at) as last_activity",
|
|
161
|
+
"count(lesli_audit_user_journals.id) requests"
|
|
160
162
|
)
|
|
161
163
|
|
|
162
|
-
|
|
164
|
+
user_journals = user_journals.group("lesli_users.id", group_by)
|
|
163
165
|
|
|
164
|
-
|
|
166
|
+
user_journals.map do |request|
|
|
165
167
|
request[:first_activity] = Date2.new(request[:first_activity]).time
|
|
166
168
|
request[:last_activity] = Date2.new(request[:last_activity]).time
|
|
167
169
|
request
|
|
168
170
|
end
|
|
169
171
|
|
|
170
|
-
|
|
172
|
+
user_journals.reverse
|
|
171
173
|
end
|
|
172
174
|
|
|
173
175
|
def registrations
|