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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/lesli_audit/application.tailwind.css +2 -0
  3. data/app/controllers/lesli_audit/users_controller.rb +1 -2
  4. data/app/controllers/lesli_audit/visitors_controller.rb +14 -0
  5. data/app/interfaces/lesli_audit/logger_interface.rb +22 -29
  6. data/app/models/concerns/lesli_audit/user_associations.rb +11 -0
  7. data/app/services/lesli_audit/user_service.rb +105 -12
  8. data/app/services/lesli_audit/{analytic_service.rb → visitor_service.rb} +121 -24
  9. data/app/views/lesli_audit/partials/_navigation.html.erb +1 -1
  10. data/app/views/lesli_audit/requests/index.html.erb +1 -1
  11. data/app/views/lesli_audit/users/index.html.erb +75 -37
  12. data/app/views/lesli_audit/visitors/index.html.erb +164 -0
  13. data/config/routes.rb +1 -1
  14. data/db/migrate/v1.0/0501110210_create_lesli_audit_account_devices.rb +2 -1
  15. data/db/seed/devices.rb +23 -15
  16. data/db/seed/users.rb +117 -0
  17. data/db/seeds.rb +2 -2
  18. data/lib/lesli_audit/engine.rb +6 -0
  19. data/lib/lesli_audit/version.rb +2 -2
  20. data/readme.md +113 -49
  21. metadata +12 -50
  22. data/app/assets/config/lesli_audit_manifest.js +0 -37
  23. data/app/controllers/lesli_audit/analytics_controller.rb +0 -23
  24. data/app/controllers/lesli_audit/user_journals_controller.rb +0 -60
  25. data/app/helpers/lesli_audit/analytics_helper.rb +0 -4
  26. data/app/views/lesli_audit/analytics/index.html.erb +0 -44
  27. data/app/views/lesli_audit/user_journals/_form.html.erb +0 -17
  28. data/app/views/lesli_audit/user_journals/_user_journal.html.erb +0 -2
  29. data/app/views/lesli_audit/user_journals/edit.html.erb +0 -12
  30. data/app/views/lesli_audit/user_journals/index.html.erb +0 -16
  31. data/app/views/lesli_audit/user_journals/new.html.erb +0 -11
  32. data/app/views/lesli_audit/user_journals/show.html.erb +0 -10
  33. data/app/views/lesli_audit/users/show.html.erb +0 -10
  34. data/lib/scss/application.scss +0 -31
  35. data/lib/vue/application.js +0 -146
  36. data/lib/vue/apps/accounts/activities.vue +0 -48
  37. data/lib/vue/apps/analytics/index.vue +0 -56
  38. data/lib/vue/apps/analytics/trends.vue +0 -148
  39. data/lib/vue/apps/dashboards/components/roles.vue +0 -71
  40. data/lib/vue/apps/dashboards/components/users.vue +0 -71
  41. data/lib/vue/apps/dashboards/show.vue +0 -8
  42. data/lib/vue/apps/requests/index.vue +0 -125
  43. data/lib/vue/apps/security/passwords.vue +0 -52
  44. data/lib/vue/apps/security/sessions.vue +0 -63
  45. data/lib/vue/apps/users/activities.vue +0 -49
  46. data/lib/vue/apps/users/index.vue +0 -89
  47. data/lib/vue/apps/users/logs.vue +0 -49
  48. data/lib/vue/apps/users/registrations.vue +0 -73
  49. data/lib/vue/apps/users/roles.vue +0 -70
  50. data/lib/vue/apps/users/workingHours.vue +0 -52
  51. data/lib/vue/components/requests.vue +0 -63
  52. data/lib/vue/components/visitors.vue +0 -76
  53. data/lib/vue/stores/accounts/activities.js +0 -87
  54. data/lib/vue/stores/analytics.js +0 -127
  55. data/lib/vue/stores/request.js +0 -70
  56. data/lib/vue/stores/security/password.js +0 -75
  57. data/lib/vue/stores/security/session.js +0 -78
  58. data/lib/vue/stores/translations.json +0 -162
  59. data/lib/vue/stores/users/activities.js +0 -76
  60. data/lib/vue/stores/users/logs.js +0 -74
  61. data/lib/vue/stores/users/registrations.js +0 -61
  62. data/lib/vue/stores/users/roles.js +0 -52
  63. data/lib/vue/stores/users/working_hours.js +0 -92
  64. data/lib/vue/stores/users.js +0 -56
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff9465d4d130455ae1ef9804d86e17e54754708857234684a0d99237f8545770
4
- data.tar.gz: 3455cb58ed202920a07a0faacf21ced66b421201a2832206e22c0495bff8d05c
3
+ metadata.gz: 1ebf3748b965f8b36dcd0861b281f5fabf373d59e71661651b01cece8bdc4c18
4
+ data.tar.gz: 712eefe307ebef94bbc2b03f94ecfa608303d30d58688c99ed303fa248b7b8d0
5
5
  SHA512:
6
- metadata.gz: 32ea636b6528c49201ba30eb2e972deaeca0ea47bf625f5e8815a81fec5dbb83bdb3c4f450c014cb33f3eef2b9e5b040acaf8a887410d3117880e89a3736dfc6
7
- data.tar.gz: 21ee15b705797e0f6871a7432b7044b85bdaee377b376b55f568d77a329e9be51bcfc6426c80290c4ec0c08dab47fa2b8da11024c59751d3af23e7ce0e8c9256
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
- @requests = UserService.new(current_user, query).requests
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 = UserAgent.parse(http_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
- if as_string == false
46
- return {
47
- platform: user_agent.platform,
48
- os: user_agent.os,
49
- browser: user_agent.browser,
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
- ) if Lesli.config.audit.dig(:enable_analytics)
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 requests
37
- current_user.account.audit.user_requests
38
- .joins(:user)
39
- .group(:email)
40
- .limit(5)
41
- .order("requests DESC")
42
- .select(
43
- :email,
44
- "count(lesli_audit_user_requests.id) resources",
45
- "sum(lesli_audit_user_requests.request_count) requests"
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 AnalyticService < Lesli::ApplicationLesliService
34
+ class VisitorService < Lesli::ApplicationLesliService
35
35
 
36
- LIMIT=5
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").limit(30)
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
- #Rails.cache.fetch(cache_key_for_account(__method__), expires_in: 4.hour) do
85
- current_user.account.audit.account_devices
86
- .group(:agent_platform, :created_at)
87
- .select(
88
- 'created_at as xaxiskey',
89
- 'agent_platform as dataname',
90
- 'sum(agent_count) as yaxiskey'
91
- ).as_json
92
- #end
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
- 'created_at as xaxiskey',
101
- 'agent_browser as dataname',
102
- 'sum(agent_count) as yaxiskey'
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.analytics_path, 'Analytics', 'ri-pie-chart-line'); %>
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 LesliView::Components::Toolbar.new() %>
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
- <div class="columns">
6
- <div class="column">
7
- <%= render(LesliView::Charts::Bar.new(
8
- title:"Registered users",
9
- database_to_dataset: @registrations.as_json
10
- ))%>
11
- </div>
12
- </div>
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
- <div class="columns">
15
- <div class="column">
16
- <%= render(LesliView::Elements::Table.new(
17
- columns: [{
18
- label: "id",
19
- field: "user_id"
20
- },{
21
- label: "User",
22
- field: "user_email"
23
- },{
24
- label:"First activity",
25
- field:"first_activity"
26
- },{
27
- label:"Last activity",
28
- field:"last_activity"
29
- },{
30
- label:"Resources",
31
- field:"resources"
32
- },{
33
- label:"Requests",
34
- field:"requests"
35
- },{
36
- label:"Date",
37
- field:"date"
38
- }],
39
- records: @working_hours
40
- )) %>
41
- </div>
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 %>