lesli_audit 1.0.2 → 1.1.1
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/app/assets/stylesheets/lesli_audit/application.tailwind.css +2 -0
- data/app/controllers/lesli_audit/users_controller.rb +1 -2
- data/app/controllers/lesli_audit/visitors_controller.rb +14 -0
- data/app/interfaces/lesli_audit/logger_interface.rb +22 -29
- data/app/models/concerns/lesli_audit/user_associations.rb +11 -0
- data/app/services/lesli_audit/user_service.rb +105 -12
- data/app/services/lesli_audit/{analytic_service.rb → visitor_service.rb} +121 -24
- data/app/views/lesli_audit/partials/_navigation.html.erb +1 -1
- data/app/views/lesli_audit/requests/index.html.erb +1 -1
- data/app/views/lesli_audit/users/index.html.erb +75 -37
- data/app/views/lesli_audit/visitors/index.html.erb +164 -0
- data/config/routes.rb +1 -1
- data/db/migrate/v1.0/0501110210_create_lesli_audit_account_devices.rb +2 -1
- data/db/seed/devices.rb +23 -15
- data/db/seed/users.rb +117 -0
- data/db/seeds.rb +2 -2
- data/lib/lesli_audit/engine.rb +6 -0
- data/lib/lesli_audit/version.rb +2 -2
- data/readme.md +113 -49
- metadata +12 -50
- data/app/assets/config/lesli_audit_manifest.js +0 -37
- data/app/controllers/lesli_audit/analytics_controller.rb +0 -23
- data/app/controllers/lesli_audit/user_journals_controller.rb +0 -60
- data/app/helpers/lesli_audit/analytics_helper.rb +0 -4
- data/app/views/lesli_audit/analytics/index.html.erb +0 -44
- data/app/views/lesli_audit/user_journals/_form.html.erb +0 -17
- data/app/views/lesli_audit/user_journals/_user_journal.html.erb +0 -2
- data/app/views/lesli_audit/user_journals/edit.html.erb +0 -12
- data/app/views/lesli_audit/user_journals/index.html.erb +0 -16
- data/app/views/lesli_audit/user_journals/new.html.erb +0 -11
- data/app/views/lesli_audit/user_journals/show.html.erb +0 -10
- data/app/views/lesli_audit/users/show.html.erb +0 -10
- data/lib/scss/application.scss +0 -31
- data/lib/vue/application.js +0 -146
- data/lib/vue/apps/accounts/activities.vue +0 -48
- data/lib/vue/apps/analytics/index.vue +0 -56
- data/lib/vue/apps/analytics/trends.vue +0 -148
- data/lib/vue/apps/dashboards/components/roles.vue +0 -71
- data/lib/vue/apps/dashboards/components/users.vue +0 -71
- data/lib/vue/apps/dashboards/show.vue +0 -8
- data/lib/vue/apps/requests/index.vue +0 -125
- data/lib/vue/apps/security/passwords.vue +0 -52
- data/lib/vue/apps/security/sessions.vue +0 -63
- data/lib/vue/apps/users/activities.vue +0 -49
- data/lib/vue/apps/users/index.vue +0 -89
- data/lib/vue/apps/users/logs.vue +0 -49
- data/lib/vue/apps/users/registrations.vue +0 -73
- data/lib/vue/apps/users/roles.vue +0 -70
- data/lib/vue/apps/users/workingHours.vue +0 -52
- data/lib/vue/components/requests.vue +0 -63
- data/lib/vue/components/visitors.vue +0 -76
- data/lib/vue/stores/accounts/activities.js +0 -87
- data/lib/vue/stores/analytics.js +0 -127
- data/lib/vue/stores/request.js +0 -70
- data/lib/vue/stores/security/password.js +0 -75
- data/lib/vue/stores/security/session.js +0 -78
- data/lib/vue/stores/translations.json +0 -162
- data/lib/vue/stores/users/activities.js +0 -76
- data/lib/vue/stores/users/logs.js +0 -74
- data/lib/vue/stores/users/registrations.js +0 -61
- data/lib/vue/stores/users/roles.js +0 -52
- data/lib/vue/stores/users/working_hours.js +0 -92
- data/lib/vue/stores/users.js +0 -56
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ebf3748b965f8b36dcd0861b281f5fabf373d59e71661651b01cece8bdc4c18
|
|
4
|
+
data.tar.gz: 712eefe307ebef94bbc2b03f94ecfa608303d30d58688c99ed303fa248b7b8d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 372f1ff92fd88a88d80ae01a94fe8d2f5f5c754922cc7bbe596f6207cac738fe7799762b9393457c2b6806c5dafd0fb1d0fa04bc9e88ce2eddb6c804d25a19a8
|
|
7
|
+
data.tar.gz: ab4e0943748344c2fedeb6b32f70e241f710a2ec28f95b730d986a0a486da83ca95ac2f6d81aa9fabd18ba8f41f6f9cd3cf658696a6d418df58e293f95344958
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--color-amber-600:oklch(66.6% .179 58.318);--color-sky-600:oklch(58.8% .158 241.966);--color-rose-600:oklch(58.6% .253 17.585);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-900:oklch(20.8% .042 265.755);--color-white:#fff;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--radius-2xl:1rem}}@layer utilities{.absolute{position:absolute}.relative{position:relative}.inset-y-0{inset-block:0}.left-0{left:0}.isolate{isolation:isolate}.-z-10{z-index:calc(10 * -1)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-6{margin-top:calc(var(--spacing) * 6)}.flex{display:flex}.grid{display:grid}.inline-flex{display:inline-flex}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-full{width:100%}.\!min-w-0{min-width:0!important}.min-w-0{min-width:0}.shrink-0{flex-shrink:0}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing) * 2)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.rounded-2xl{border-radius:var(--radius-2xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-slate-200{border-color:var(--color-slate-200)}.bg-slate-100{background-color:var(--color-slate-100)}.bg-white{background-color:var(--color-white)}.\!p-0{padding:0!important}.p-5{padding:calc(var(--spacing) * 5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.pb-8{padding-bottom:calc(var(--spacing) * 8)}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.leading-none{--tw-leading:1;line-height:1}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.text-amber-600{color:var(--color-amber-600)}.text-rose-600{color:var(--color-rose-600)}.text-sky-600{color:var(--color-sky-600)}.text-slate-500{color:var(--color-slate-500)}.text-slate-700{color:var(--color-slate-700)}.text-slate-900{color:var(--color-slate-900)}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}@media (min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:64rem){.lg\:w-2\/5{width:40%}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
|
|
@@ -34,8 +34,7 @@ module LesliAudit
|
|
|
34
34
|
|
|
35
35
|
# GET /users
|
|
36
36
|
def index
|
|
37
|
-
@
|
|
38
|
-
@registrations = UserService.new(current_user, query).registrations
|
|
37
|
+
@users = UserService.new(current_user, query).users
|
|
39
38
|
@working_hours = UserService.new(current_user, query).working_hours
|
|
40
39
|
end
|
|
41
40
|
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module LesliAudit
|
|
2
|
+
class VisitorsController < ApplicationController
|
|
3
|
+
|
|
4
|
+
# GET /analytics
|
|
5
|
+
def index
|
|
6
|
+
@visits = VisitorService.new(current_user, query).visits
|
|
7
|
+
@visitors = VisitorService.new(current_user, query).visitors
|
|
8
|
+
@requests = VisitorService.new(current_user, query).requests
|
|
9
|
+
@browsers = VisitorService.new(current_user, query).browsers
|
|
10
|
+
@devices = VisitorService.new(current_user, query).devices
|
|
11
|
+
@os = VisitorService.new(current_user, query).os
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -29,35 +29,28 @@ Building a better future, one line of code at a time.
|
|
|
29
29
|
// · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
|
|
30
30
|
// ·
|
|
31
31
|
=end
|
|
32
|
-
|
|
32
|
+
require 'device_detector'
|
|
33
33
|
module LesliAudit
|
|
34
34
|
module LoggerInterface
|
|
35
35
|
def get_user_agent(as_string=true)
|
|
36
36
|
|
|
37
|
-
http_user_agent = request.env["HTTP_USER_AGENT"]
|
|
38
|
-
|
|
39
37
|
# parse user agent
|
|
40
|
-
user_agent =
|
|
38
|
+
user_agent = DeviceDetector.new(request.env["HTTP_USER_AGENT"])
|
|
41
39
|
|
|
42
|
-
user_agent_version = user_agent.version.to_a.first(2).join(".")
|
|
40
|
+
#user_agent_version = user_agent.version.to_a.first(2).join(".")
|
|
43
41
|
|
|
44
42
|
# return user agent as object
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
version: user_agent_version
|
|
51
|
-
}
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# return user agent info as string
|
|
55
|
-
"#{user_agent.platform} #{user_agent.os} - #{user_agent.browser} #{user_agent_version}"
|
|
43
|
+
{
|
|
44
|
+
platform: user_agent.os_name,
|
|
45
|
+
browser: user_agent.name,
|
|
46
|
+
device: user_agent.device_type
|
|
47
|
+
}
|
|
56
48
|
end
|
|
57
49
|
|
|
58
50
|
def log_requests
|
|
59
51
|
log_account_requests
|
|
60
52
|
log_user_requests
|
|
53
|
+
log_devices
|
|
61
54
|
end
|
|
62
55
|
|
|
63
56
|
def log_account_requests
|
|
@@ -86,17 +79,7 @@ module LesliAudit
|
|
|
86
79
|
def log_user_requests
|
|
87
80
|
return unless current_user
|
|
88
81
|
return unless session[:user_session_id]
|
|
89
|
-
|
|
90
|
-
current_user.account.audit.user_journals.create({
|
|
91
|
-
request_controller: controller_path,
|
|
92
|
-
request_action: action_name,
|
|
93
|
-
session_id: session[:user_session_id],
|
|
94
|
-
user_id: current_user.id,
|
|
95
|
-
date: Date2.new.date.to_s
|
|
96
|
-
}) if Lesli.config.audit.dig(:enable_journals)
|
|
97
|
-
|
|
98
|
-
# Determine the correct SQL "now" keyword based on the database connection
|
|
99
|
-
now_func = ActiveRecord::Base.connection.adapter_name =~ /sqlite/i ? 'CURRENT_TIMESTAMP' : 'NOW()'
|
|
82
|
+
return unless Lesli.config.audit.dig(:enable_analytics)
|
|
100
83
|
|
|
101
84
|
# Try to save a unique record for this request configuration
|
|
102
85
|
current_user.account.audit.user_requests.upsert(
|
|
@@ -118,7 +101,16 @@ module LesliAudit
|
|
|
118
101
|
on_duplicate: Arel.sql(
|
|
119
102
|
"request_count = lesli_audit_user_requests.request_count + 1,updated_at = #{LesliDate::Compatibility.db_now}"
|
|
120
103
|
)
|
|
121
|
-
)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return unless Lesli.config.audit.dig(:enable_journals)
|
|
107
|
+
current_user.account.audit.user_journals.create({
|
|
108
|
+
request_controller: controller_path,
|
|
109
|
+
request_action: action_name,
|
|
110
|
+
session_id: session[:user_session_id],
|
|
111
|
+
user_id: current_user.id,
|
|
112
|
+
date: Date2.new.date.to_s
|
|
113
|
+
})
|
|
122
114
|
end
|
|
123
115
|
|
|
124
116
|
def log_devices
|
|
@@ -133,11 +125,12 @@ module LesliAudit
|
|
|
133
125
|
:created_at => Date2.new.date.to_s,
|
|
134
126
|
:agent_platform => user_agent&.dig(:platform) || "unknown",
|
|
135
127
|
:agent_browser => user_agent&.dig(:browser) || "unknown",
|
|
128
|
+
:agent_device => user_agent&.dig(:device) || "unknown",
|
|
136
129
|
:agent_count => 1
|
|
137
130
|
},
|
|
138
131
|
|
|
139
132
|
# group of columns to consider a request as unique
|
|
140
|
-
unique_by: %i[agent_platform agent_browser created_at account_id],
|
|
133
|
+
unique_by: %i[agent_platform agent_browser agent_device created_at account_id],
|
|
141
134
|
|
|
142
135
|
# if request id is not unique
|
|
143
136
|
# - increase the counter for this configuration
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module LesliAudit
|
|
2
|
+
module UserAssociations
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
has_many :requests, class_name: "LesliAudit::UserRequest"
|
|
7
|
+
has_many :journals, class_name: "LesliAudit::UserJournal"
|
|
8
|
+
has_many :logs, class_name: "LesliAudit::UserLog"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -33,17 +33,104 @@ Building a better future, one line of code at a time.
|
|
|
33
33
|
module LesliAudit
|
|
34
34
|
class UserService < Lesli::ApplicationLesliService
|
|
35
35
|
|
|
36
|
-
def
|
|
37
|
-
current_user.account.
|
|
38
|
-
|
|
39
|
-
.
|
|
40
|
-
.
|
|
41
|
-
|
|
42
|
-
.
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
|
|
36
|
+
def users
|
|
37
|
+
users = current_user.account.users
|
|
38
|
+
|
|
39
|
+
now = Time.current
|
|
40
|
+
last_30_days = 30.days.ago
|
|
41
|
+
|
|
42
|
+
stats = users.pick(
|
|
43
|
+
# total users
|
|
44
|
+
Arel.sql("COUNT(*)"),
|
|
45
|
+
|
|
46
|
+
# total deactivated users
|
|
47
|
+
Arel.sql("SUM(CASE WHEN active = 0 AND deleted_at IS NULL THEN 1 ELSE 0 END)"),
|
|
48
|
+
|
|
49
|
+
# total locked users
|
|
50
|
+
Arel.sql(
|
|
51
|
+
"SUM(CASE WHEN (
|
|
52
|
+
locked_at IS NOT NULL
|
|
53
|
+
OR locked_until > #{ActiveRecord::Base.connection.quote(Time.current)}
|
|
54
|
+
) AND deleted_at IS NULL THEN 1 ELSE 0 END)"
|
|
55
|
+
),
|
|
56
|
+
|
|
57
|
+
# total not confirmed users
|
|
58
|
+
Arel.sql("SUM(CASE WHEN confirmed_at IS NULL AND deleted_at IS NULL THEN 1 ELSE 0 END)"),
|
|
59
|
+
|
|
60
|
+
# total deleted users
|
|
61
|
+
Arel.sql("SUM(CASE WHEN deleted_at IS NOT NULL THEN 1 ELSE 0 END)"),
|
|
62
|
+
|
|
63
|
+
# users that never signed in
|
|
64
|
+
Arel.sql("SUM(CASE WHEN last_sign_in_at IS NULL AND deleted_at IS NULL THEN 1 ELSE 0 END)"),
|
|
65
|
+
|
|
66
|
+
# total active users for the last 30 days
|
|
67
|
+
Arel.sql("SUM(CASE WHEN last_sign_in_at >= #{ActiveRecord::Base.connection.quote(last_30_days)} AND deleted_at IS NULL THEN 1 ELSE 0 END)"),
|
|
68
|
+
|
|
69
|
+
# users with failed logins
|
|
70
|
+
Arel.sql("SUM(CASE WHEN failed_attempts > 0 AND deleted_at IS NULL THEN 1 ELSE 0 END)"),
|
|
46
71
|
)
|
|
72
|
+
|
|
73
|
+
summary = {
|
|
74
|
+
total_users: stats[0].to_i,
|
|
75
|
+
total_users_deactivated: stats[1].to_i,
|
|
76
|
+
total_users_locked: stats[2].to_i,
|
|
77
|
+
total_users_unconfirmed: stats[3].to_i,
|
|
78
|
+
total_users_deleted: stats[4].to_i,
|
|
79
|
+
|
|
80
|
+
total_users_never_signed_in: stats[5].to_i,
|
|
81
|
+
total_users_active_last_30_days: stats[6].to_i,
|
|
82
|
+
total_users_with_failed_attempts: stats[7].to_i
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
{
|
|
86
|
+
overview: [{
|
|
87
|
+
label: "Total users",
|
|
88
|
+
value: summary[:total_users],
|
|
89
|
+
percentage: 100,
|
|
90
|
+
color: "info"
|
|
91
|
+
}, {
|
|
92
|
+
label: "Deactivated users",
|
|
93
|
+
value: summary[:total_users_deactivated],
|
|
94
|
+
percentage: percentage(summary[:total_users_deactivated], summary[:total_users]),
|
|
95
|
+
color: "warning"
|
|
96
|
+
}, {
|
|
97
|
+
label: "Locked users",
|
|
98
|
+
value: summary[:total_users_locked],
|
|
99
|
+
percentage: percentage(summary[:total_users_locked], summary[:total_users]),
|
|
100
|
+
color: "danger"
|
|
101
|
+
}, {
|
|
102
|
+
label: "Unconfirmed users",
|
|
103
|
+
value: summary[:total_users_unconfirmed],
|
|
104
|
+
percentage: percentage(summary[:total_users_unconfirmed], summary[:total_users]),
|
|
105
|
+
color: "warning",
|
|
106
|
+
}, {
|
|
107
|
+
label: "Deleted users",
|
|
108
|
+
value: summary[:total_users_deleted],
|
|
109
|
+
percentage: percentage(summary[:total_users_deleted], summary[:total_users]),
|
|
110
|
+
color: "danger"
|
|
111
|
+
}],
|
|
112
|
+
attention_needed: [{
|
|
113
|
+
label: "Unconfirmed users",
|
|
114
|
+
value: summary[:total_users_unconfirmed],
|
|
115
|
+
color: "info",
|
|
116
|
+
icon: "user-add-line"
|
|
117
|
+
}, {
|
|
118
|
+
label: "Never signed in",
|
|
119
|
+
value: summary[:total_users_never_signed_in],
|
|
120
|
+
color: "warning",
|
|
121
|
+
icon: "time-line"
|
|
122
|
+
}, {
|
|
123
|
+
label: "Failed login attempts",
|
|
124
|
+
value: summary[:total_users_with_failed_attempts],
|
|
125
|
+
color: "danger",
|
|
126
|
+
icon: "shield-keyhole-line"
|
|
127
|
+
}, {
|
|
128
|
+
label: "Locked users",
|
|
129
|
+
value: summary[:total_users_locked],
|
|
130
|
+
color: "danger",
|
|
131
|
+
icon: "lock-line"
|
|
132
|
+
}]
|
|
133
|
+
}
|
|
47
134
|
end
|
|
48
135
|
|
|
49
136
|
def working_hours
|
|
@@ -72,8 +159,7 @@ module LesliAudit
|
|
|
72
159
|
"sum(lesli_audit_user_requests.request_count) requests"
|
|
73
160
|
)
|
|
74
161
|
|
|
75
|
-
user_requests = user_requests
|
|
76
|
-
.group(:user_id, group_by)
|
|
162
|
+
user_requests = user_requests.group(:user_id, group_by)
|
|
77
163
|
|
|
78
164
|
user_requests.map do |request|
|
|
79
165
|
request[:first_activity] = Date2.new(request[:first_activity]).time
|
|
@@ -118,5 +204,12 @@ module LesliAudit
|
|
|
118
204
|
|
|
119
205
|
registrations
|
|
120
206
|
end
|
|
207
|
+
|
|
208
|
+
private
|
|
209
|
+
|
|
210
|
+
def percentage(value, total)
|
|
211
|
+
return 0 if total.to_i.zero?
|
|
212
|
+
((value.to_f / total) * 100).round
|
|
213
|
+
end
|
|
121
214
|
end
|
|
122
215
|
end
|
|
@@ -31,15 +31,22 @@ Building a better future, one line of code at a time.
|
|
|
31
31
|
=end
|
|
32
32
|
|
|
33
33
|
module LesliAudit
|
|
34
|
-
class
|
|
34
|
+
class VisitorService < Lesli::ApplicationLesliService
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
def visits
|
|
37
|
+
requests = current_user.account.audit.account_requests
|
|
38
|
+
|
|
39
|
+
controllers, total_requests = requests.pick(
|
|
40
|
+
Arel.sql("COUNT(DISTINCT request_controller)"),
|
|
41
|
+
Arel.sql("SUM(request_count)")
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
controllers: controllers.to_i,
|
|
46
|
+
requests: total_requests.to_i
|
|
47
|
+
}
|
|
48
|
+
end
|
|
37
49
|
|
|
38
|
-
# @overwrite
|
|
39
|
-
# @return {Hash} Paginated list of the records
|
|
40
|
-
# @param {query} Has of the formated queries/filters that will be applied to filter data
|
|
41
|
-
# @description
|
|
42
|
-
# @example
|
|
43
50
|
def visitors
|
|
44
51
|
#Rails.cache.fetch(cache_key_for_account(__method__), expires_in: 1.hour) do
|
|
45
52
|
group = 'day'
|
|
@@ -70,7 +77,7 @@ module LesliAudit
|
|
|
70
77
|
|
|
71
78
|
def requests
|
|
72
79
|
requests = current_user.account.audit.account_requests
|
|
73
|
-
.group("request_controller")
|
|
80
|
+
.group("request_controller")
|
|
74
81
|
|
|
75
82
|
requests = apply_filters(requests, query)
|
|
76
83
|
|
|
@@ -80,32 +87,122 @@ module LesliAudit
|
|
|
80
87
|
).as_json
|
|
81
88
|
end
|
|
82
89
|
|
|
90
|
+
def os
|
|
91
|
+
device_counts = current_user.account.audit.account_devices
|
|
92
|
+
.group(:agent_platform)
|
|
93
|
+
.sum(:agent_count)
|
|
94
|
+
|
|
95
|
+
total = device_counts.values.sum
|
|
96
|
+
|
|
97
|
+
device_counts.map do |device, count|
|
|
98
|
+
{
|
|
99
|
+
device: device,
|
|
100
|
+
count: count,
|
|
101
|
+
icon: platform_icon(device),
|
|
102
|
+
percentage: total.zero? ? 0 : ((count.to_f / total) * 100).round()
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
83
107
|
def devices
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
108
|
+
device_counts = current_user.account.audit.account_devices
|
|
109
|
+
.group(:agent_device)
|
|
110
|
+
.sum(:agent_count)
|
|
111
|
+
|
|
112
|
+
total = device_counts.values.sum
|
|
113
|
+
|
|
114
|
+
device_counts.map do |device, count|
|
|
115
|
+
{
|
|
116
|
+
device: device,
|
|
117
|
+
count: count,
|
|
118
|
+
icon: device_icon(device),
|
|
119
|
+
percentage: total.zero? ? 0 : ((count.to_f / total) * 100).round()
|
|
120
|
+
}
|
|
121
|
+
end
|
|
93
122
|
end
|
|
94
123
|
|
|
95
124
|
def browsers
|
|
125
|
+
browser_counts = current_user.account.audit.account_devices
|
|
126
|
+
.group(:agent_browser)
|
|
127
|
+
.sum(:agent_count)
|
|
128
|
+
|
|
129
|
+
total = browser_counts.values.sum
|
|
130
|
+
|
|
131
|
+
browser_counts.map do |browser, count|
|
|
132
|
+
{
|
|
133
|
+
browser: browser,
|
|
134
|
+
count: count,
|
|
135
|
+
icon: browser_icon(browser),
|
|
136
|
+
percentage: total.zero? ? 0 : ((count.to_f / total) * 100).round()
|
|
137
|
+
}
|
|
138
|
+
end
|
|
96
139
|
#Rails.cache.fetch(cache_key_for_account(__method__), expires_in: 4.hour) do
|
|
97
|
-
current_user.account.audit.account_devices
|
|
98
|
-
.group(:agent_browser, :created_at)
|
|
99
|
-
.select(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
).as_json
|
|
140
|
+
# current_user.account.audit.account_devices
|
|
141
|
+
# .group(:agent_browser, :created_at)
|
|
142
|
+
# .select(
|
|
143
|
+
# 'created_at as xaxiskey',
|
|
144
|
+
# 'agent_browser as dataname',
|
|
145
|
+
# 'sum(agent_count) as yaxiskey'
|
|
146
|
+
# ).as_json
|
|
104
147
|
#end
|
|
105
148
|
end
|
|
106
149
|
|
|
107
150
|
private
|
|
108
151
|
|
|
152
|
+
LIMIT=15
|
|
153
|
+
|
|
154
|
+
DEFAULT_PLATFORM_ICON_CLASS = "computer-fill"
|
|
155
|
+
PLATFORM_ICON_CLASSES = {
|
|
156
|
+
/chrome os|cros/i => "chrome-fill",
|
|
157
|
+
/ios|ipad|iphone|mac/i => "apple-fill",
|
|
158
|
+
/windows/i => "windows-fill",
|
|
159
|
+
/ubuntu/i => "ubuntu-fill",
|
|
160
|
+
/debian|fedora|linux/i => "linux-fill",
|
|
161
|
+
/android/i => "android-fill"
|
|
162
|
+
}.freeze
|
|
163
|
+
|
|
164
|
+
DEFAULT_BROWSER_ICON_CLASS = "global-fill"
|
|
165
|
+
BROWSER_ICON_CLASSES = {
|
|
166
|
+
/chrome|chromium/i => "chrome-fill",
|
|
167
|
+
/firefox/i => "firefox-fill",
|
|
168
|
+
/safari/i => "safari-fill",
|
|
169
|
+
/edge/i => "edge-fill",
|
|
170
|
+
/opera|opr/i => "opera-fill",
|
|
171
|
+
/internet explorer|msie/i => "ie-fill",
|
|
172
|
+
/brave/i => "brave-fill"
|
|
173
|
+
}.freeze
|
|
174
|
+
|
|
175
|
+
DEVICE_ICON_CLASSES = {
|
|
176
|
+
"desktop" => "mac-line",
|
|
177
|
+
"tablet" => "tablet-line",
|
|
178
|
+
"smartphone" => "smartphone-line",
|
|
179
|
+
"unknown" => "file-unknow-line"
|
|
180
|
+
}.freeze
|
|
181
|
+
|
|
182
|
+
def platform_icon(platform)
|
|
183
|
+
return DEFAULT_PLATFORM_ICON_CLASS if platform.blank?
|
|
184
|
+
|
|
185
|
+
match = PLATFORM_ICON_CLASSES.find do |pattern, _icon_class|
|
|
186
|
+
platform.match?(pattern)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
match ? match.last : DEFAULT_PLATFORM_ICON_CLASS
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def browser_icon(browser)
|
|
193
|
+
return DEFAULT_BROWSER_ICON_CLASS if browser.blank?
|
|
194
|
+
|
|
195
|
+
match = BROWSER_ICON_CLASSES.find do |pattern, _icon_class|
|
|
196
|
+
browser.match?(pattern)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
match ? match.last : DEFAULT_BROWSER_ICON_CLASS
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def device_icon(device)
|
|
203
|
+
DEVICE_ICON_CLASSES.fetch(device.to_s, DEVICE_ICON_CLASSES["unknown"])
|
|
204
|
+
end
|
|
205
|
+
|
|
109
206
|
def apply_filters requests, params
|
|
110
207
|
|
|
111
208
|
return requests
|
|
@@ -33,6 +33,6 @@ Building a better future, one line of code at a time.
|
|
|
33
33
|
%>
|
|
34
34
|
|
|
35
35
|
<%= navigation_item(lesli_audit.users_path, 'Users', 'ri-team-line'); %>
|
|
36
|
-
<%= navigation_item(lesli_audit.
|
|
36
|
+
<%= navigation_item(lesli_audit.visitors_path, 'Visitors', 'ri-line-chart-fill'); %>
|
|
37
37
|
<%= navigation_item(lesli_audit.requests_path, 'Requests', 'ri-arrow-left-right-fill'); %>
|
|
38
38
|
<%= navigation_item(lesli_audit.logs_path, 'Logs', 'ri-history-line'); %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<%= render(LesliView::Layout::Container.new("audit-requests")) do %>
|
|
2
2
|
<%= render(LesliView::Components::Header.new("Requests")) %>
|
|
3
|
-
<%= render
|
|
3
|
+
<%= render(LesliView::Components::Toolbar.new()) %>
|
|
4
4
|
<%= render(LesliView::Elements::Table.new(
|
|
5
5
|
columns: [{
|
|
6
6
|
label: "ID",
|
|
@@ -1,43 +1,81 @@
|
|
|
1
1
|
|
|
2
|
+
<%
|
|
3
|
+
color_classes = {
|
|
4
|
+
"info" => "text-sky-600",
|
|
5
|
+
"warning" => "text-amber-600",
|
|
6
|
+
"danger" => "text-rose-600"
|
|
7
|
+
}.freeze
|
|
8
|
+
%>
|
|
9
|
+
|
|
2
10
|
<%= render(LesliView::Layout::Container.new("audit-users")) do %>
|
|
3
11
|
<%= render(LesliView::Components::Header.new("Users")) %>
|
|
4
12
|
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
<section
|
|
14
|
+
class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-5"
|
|
15
|
+
aria-label="User overview">
|
|
16
|
+
<% @users[:overview].each do |user| %>
|
|
17
|
+
<article class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
|
18
|
+
<h2 class="text-sm font-medium text-slate-500"><%= user[:label] %></h2>
|
|
19
|
+
<p class="mt-3 text-3xl font-semibold tracking-tight tabular-nums <%= color_classes.fetch(user[:color], "text-slate-700") %>">
|
|
20
|
+
<%= user[:value] %>
|
|
21
|
+
</p>
|
|
22
|
+
</article>
|
|
23
|
+
<% end %>
|
|
24
|
+
</section>
|
|
25
|
+
|
|
26
|
+
<section class="mt-6 lg:w-2/5" aria-label="Users needing attention">
|
|
27
|
+
<%= render(LesliView::Elements::Table.new(
|
|
28
|
+
columns: [],
|
|
29
|
+
headless: true,
|
|
30
|
+
striped: false,
|
|
31
|
+
compact: true,
|
|
32
|
+
class_name: "!min-w-0",
|
|
33
|
+
caption: "Users needing attention"
|
|
34
|
+
)) do |table| %>
|
|
35
|
+
<% @users[:attention_needed].each do |user| %>
|
|
36
|
+
<% table.with_row do |row| %>
|
|
37
|
+
<% row.with_cell do %>
|
|
38
|
+
<span class="inline-flex h-8 w-8 items-center justify-center <%= color_classes.fetch(user[:color], "text-slate-500") %>">
|
|
39
|
+
<span class="ri-<%= user[:icon] %> text-xl leading-none" aria-hidden="true"></span>
|
|
40
|
+
</span>
|
|
41
|
+
<% end %>
|
|
42
|
+
<% row.with_cell(class_name: "w-full font-medium text-slate-700") do %>
|
|
43
|
+
<%= user[:label] %>
|
|
44
|
+
<% end %>
|
|
45
|
+
<% row.with_cell(class_name: "font-semibold tabular-nums text-slate-900", align: :right) do %>
|
|
46
|
+
<%= user[:value] %>
|
|
47
|
+
<% end %>
|
|
48
|
+
<% end %>
|
|
49
|
+
<% end %>
|
|
50
|
+
<% end %>
|
|
51
|
+
</section>
|
|
13
52
|
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
</div>
|
|
53
|
+
<section class="mt-6 pb-8" aria-label="User working hours">
|
|
54
|
+
<%= render(LesliView::Elements::Table.new(
|
|
55
|
+
columns: [{
|
|
56
|
+
label: "id",
|
|
57
|
+
field: "user_id"
|
|
58
|
+
}, {
|
|
59
|
+
label: "User",
|
|
60
|
+
field: "user_email"
|
|
61
|
+
}, {
|
|
62
|
+
label: "First activity",
|
|
63
|
+
field: "first_activity"
|
|
64
|
+
}, {
|
|
65
|
+
label: "Last activity",
|
|
66
|
+
field: "last_activity"
|
|
67
|
+
}, {
|
|
68
|
+
label: "Resources",
|
|
69
|
+
field: "resources"
|
|
70
|
+
}, {
|
|
71
|
+
label: "Requests",
|
|
72
|
+
field: "requests"
|
|
73
|
+
}, {
|
|
74
|
+
label: "Date",
|
|
75
|
+
field: "date"
|
|
76
|
+
}],
|
|
77
|
+
records: @working_hours,
|
|
78
|
+
caption: "User working hours"
|
|
79
|
+
)) %>
|
|
80
|
+
</section>
|
|
43
81
|
<% end %>
|