trackguard 0.17.0 → 0.20.0
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/images/trackguard/logo.png +0 -0
- data/app/assets/stylesheets/trackguard/admin.css +29 -29
- data/app/controllers/concerns/trackguard/admin/overridable.rb +29 -0
- data/app/controllers/trackguard/admin/analytics_controller.rb +30 -35
- data/app/controllers/trackguard/admin/blocked_user_agents_controller.rb +1 -1
- data/app/controllers/trackguard/admin/dashboards_controller.rb +14 -8
- data/app/controllers/trackguard/admin/visitors_controller.rb +7 -13
- data/app/controllers/trackguard/admin/visits_controller.rb +7 -5
- data/app/controllers/trackguard/admin/whitelisted_ips_controller.rb +8 -14
- data/app/services/trackguard/analytics_query.rb +27 -0
- data/app/services/trackguard/page_view_recorder.rb +3 -11
- data/app/views/layouts/trackguard/admin.html.erb +1 -21
- data/lib/generators/trackguard/templates/create_trackguard_tables.rb +1 -0
- data/lib/trackguard/adapters/base.rb +19 -0
- data/lib/trackguard/adapters/local.rb +43 -0
- data/lib/trackguard/rack_attack.rb +8 -6
- data/lib/trackguard/version.rb +1 -1
- data/lib/trackguard.rb +20 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3e13b68029bc80d015c4f69531d17e5b362092d6df2d720dfb7c1563b5b4e24c
|
|
4
|
+
data.tar.gz: e676a88de14a04d1c42c0a78f7e895a7ef5011c7567e93a168e82e8f950bfeb1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d4074d349ac565380d3365ef21ca99e61836601f51fe5822cff34a3184f93cea71dc0ab541b2a08f2f0ccb49fade36c89a97f8681a5609748088ecdb661f6f84
|
|
7
|
+
data.tar.gz: 1783d02fa8caedd5816efce4cca6faab7c305af260549de8ece369c22ff47b279725e9503dc5c3d8cade1f5069516faaee594fd362e16b92939317168f4d7dfc
|
|
Binary file
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Inter", sans-serif;
|
|
8
8
|
font-size: 16px;
|
|
9
9
|
line-height: 1.5;
|
|
10
|
-
background-color: #
|
|
10
|
+
background-color: #0f0909;
|
|
11
11
|
color: #e0e0e0;
|
|
12
12
|
-webkit-font-smoothing: antialiased;
|
|
13
13
|
min-height: 100vh;
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
|
|
23
23
|
/* ── Header ────────────────────────────────────────────────────────── */
|
|
24
24
|
.tg-header {
|
|
25
|
-
background: #
|
|
26
|
-
border-bottom: 1px solid #
|
|
25
|
+
background: #190e0e;
|
|
26
|
+
border-bottom: 1px solid #2e1515;
|
|
27
27
|
padding: 0.875rem 0;
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -40,16 +40,16 @@
|
|
|
40
40
|
font-size: 0.9375rem;
|
|
41
41
|
font-weight: 600;
|
|
42
42
|
letter-spacing: 0.03em;
|
|
43
|
-
color: #
|
|
43
|
+
color: #f87171;
|
|
44
44
|
text-decoration: none;
|
|
45
45
|
flex: 1;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
.tg-brand__logo {
|
|
49
49
|
width: 20px;
|
|
50
|
-
height:
|
|
50
|
+
height: auto;
|
|
51
51
|
flex-shrink: 0;
|
|
52
|
-
filter: drop-shadow(0 0 4px rgba(
|
|
52
|
+
filter: drop-shadow(0 0 4px rgba(239, 68, 68, 0.4));
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/* ── Back link ─────────────────────────────────────────────────────── */
|
|
@@ -76,8 +76,8 @@
|
|
|
76
76
|
|
|
77
77
|
/* ── Nav ───────────────────────────────────────────────────────────── */
|
|
78
78
|
.tg-nav {
|
|
79
|
-
background: #
|
|
80
|
-
border-bottom: 1px solid #
|
|
79
|
+
background: #190e0e;
|
|
80
|
+
border-bottom: 1px solid #2e1515;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
.tg-nav > .tg-container {
|
|
@@ -97,8 +97,8 @@
|
|
|
97
97
|
.tg-nav__link:hover { color: #e0e0e0; }
|
|
98
98
|
|
|
99
99
|
.tg-nav__link--active {
|
|
100
|
-
color: #
|
|
101
|
-
border-bottom-color: #
|
|
100
|
+
color: #f87171;
|
|
101
|
+
border-bottom-color: #e53e3e;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/* ── Main ──────────────────────────────────────────────────────────── */
|
|
@@ -128,8 +128,8 @@
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
.tg-stat {
|
|
131
|
-
background: #
|
|
132
|
-
border: 1px solid #
|
|
131
|
+
background: #190e0e;
|
|
132
|
+
border: 1px solid #2e1515;
|
|
133
133
|
border-radius: 8px;
|
|
134
134
|
padding: 1.25rem;
|
|
135
135
|
}
|
|
@@ -167,8 +167,8 @@
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
.tg-panel {
|
|
170
|
-
background: #
|
|
171
|
-
border: 1px solid #
|
|
170
|
+
background: #190e0e;
|
|
171
|
+
border: 1px solid #2e1515;
|
|
172
172
|
border-radius: 8px;
|
|
173
173
|
padding: 1rem;
|
|
174
174
|
margin-bottom: 1rem;
|
|
@@ -211,14 +211,14 @@
|
|
|
211
211
|
font-size: 0.8125rem;
|
|
212
212
|
color: #e0e0e0;
|
|
213
213
|
text-decoration: none;
|
|
214
|
-
background: #
|
|
214
|
+
background: #2e1515;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
.tg-pagination__link:hover { background: #
|
|
217
|
+
.tg-pagination__link:hover { background: #3d1c1c; }
|
|
218
218
|
|
|
219
219
|
.tg-pagination__link--active {
|
|
220
|
-
background: #
|
|
221
|
-
color: #
|
|
220
|
+
background: #e53e3e;
|
|
221
|
+
color: #ffffff;
|
|
222
222
|
font-weight: 600;
|
|
223
223
|
}
|
|
224
224
|
|
|
@@ -249,14 +249,14 @@
|
|
|
249
249
|
text-transform: uppercase;
|
|
250
250
|
letter-spacing: 0.08em;
|
|
251
251
|
color: #666666;
|
|
252
|
-
border-bottom: 1px solid #
|
|
252
|
+
border-bottom: 1px solid #2e1515;
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
.tg-th--right { text-align: right; }
|
|
256
256
|
|
|
257
257
|
.tg-td {
|
|
258
258
|
padding: 0.75rem 0.875rem;
|
|
259
|
-
border-bottom: 1px solid #
|
|
259
|
+
border-bottom: 1px solid #2e1515;
|
|
260
260
|
color: #f0f0f0;
|
|
261
261
|
font-weight: 500;
|
|
262
262
|
}
|
|
@@ -271,7 +271,7 @@
|
|
|
271
271
|
|
|
272
272
|
.tg-td--bare {
|
|
273
273
|
padding: 0;
|
|
274
|
-
border-bottom: 1px solid #
|
|
274
|
+
border-bottom: 1px solid #2e1515;
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
/* ── Empty state ───────────────────────────────────────────────────── */
|
|
@@ -361,7 +361,7 @@
|
|
|
361
361
|
/* ── Detail panel ──────────────────────────────────────────────────── */
|
|
362
362
|
.tg-detail {
|
|
363
363
|
padding: 0 1rem 1rem 1rem;
|
|
364
|
-
background: #
|
|
364
|
+
background: #190e0e;
|
|
365
365
|
}
|
|
366
366
|
|
|
367
367
|
.tg-detail__grid {
|
|
@@ -423,7 +423,7 @@
|
|
|
423
423
|
align-items: center;
|
|
424
424
|
gap: 0.5rem;
|
|
425
425
|
padding: 0.75rem 0 0;
|
|
426
|
-
border-top: 1px solid #
|
|
426
|
+
border-top: 1px solid #2e1515;
|
|
427
427
|
margin-top: 0.75rem;
|
|
428
428
|
}
|
|
429
429
|
|
|
@@ -436,8 +436,8 @@
|
|
|
436
436
|
|
|
437
437
|
.tg-input {
|
|
438
438
|
flex: 1;
|
|
439
|
-
background: #
|
|
440
|
-
border: 1px solid #
|
|
439
|
+
background: #0f0909;
|
|
440
|
+
border: 1px solid #2e1515;
|
|
441
441
|
border-radius: 4px;
|
|
442
442
|
color: #e0e0e0;
|
|
443
443
|
font-size: 0.8125rem;
|
|
@@ -445,7 +445,7 @@
|
|
|
445
445
|
outline: none;
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
-
.tg-input:focus { border-color: #
|
|
448
|
+
.tg-input:focus { border-color: #e53e3e; }
|
|
449
449
|
.tg-input::placeholder { color: #444; }
|
|
450
450
|
|
|
451
451
|
.tg-btn {
|
|
@@ -461,8 +461,8 @@
|
|
|
461
461
|
.tg-btn--danger { background: #7f1d1d; color: #fca5a5; }
|
|
462
462
|
.tg-btn--danger:hover { background: #991b1b; }
|
|
463
463
|
|
|
464
|
-
.tg-btn--ghost { background: #
|
|
465
|
-
.tg-btn--ghost:hover { background: #
|
|
464
|
+
.tg-btn--ghost { background: #2e1515; color: #e0e0e0; }
|
|
465
|
+
.tg-btn--ghost:hover { background: #3d1c1c; }
|
|
466
466
|
|
|
467
467
|
.tg-btn--whitelist { background: #064e3b; color: #6ee7b7; }
|
|
468
468
|
.tg-btn--whitelist:hover { background: #065f46; }
|
|
@@ -476,4 +476,4 @@
|
|
|
476
476
|
.tg-stats { grid-template-columns: 1fr; }
|
|
477
477
|
.tg-detail__grid { grid-template-columns: 1fr; }
|
|
478
478
|
.tg-summary__ip { display: none; }
|
|
479
|
-
}
|
|
479
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Trackguard
|
|
2
|
+
module Admin
|
|
3
|
+
module Overridable
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def visitor_scope
|
|
9
|
+
Visitor.all
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def page_view_scope
|
|
13
|
+
PageView.all
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def after_action_path
|
|
17
|
+
dashboard_path
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def set_visitor
|
|
21
|
+
@visitor = if params[:ip].present?
|
|
22
|
+
visitor_scope.find_by!(ip: params[:ip])
|
|
23
|
+
else
|
|
24
|
+
visitor_scope.find(params[:id])
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -1,46 +1,25 @@
|
|
|
1
1
|
module Trackguard
|
|
2
2
|
module Admin
|
|
3
3
|
class AnalyticsController < BaseController
|
|
4
|
+
include Overridable
|
|
5
|
+
|
|
4
6
|
skip_before_action :verify_authenticity_token, if: :valid_api_token?
|
|
5
7
|
|
|
6
|
-
# rubocop:disable Metrics/AbcSize
|
|
7
8
|
def show
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
base = time_scope
|
|
13
|
-
@top_pages = base.group(:path).order("count_all DESC").limit(10).count
|
|
14
|
-
@top_referrers = base.with_referrer.group(:referer).order("count_all DESC").limit(10).count
|
|
15
|
-
@top_sources = base.with_source.group(:source).order("count_all DESC").limit(10).count
|
|
16
|
-
|
|
17
|
-
@recent = visitor_filtered(
|
|
18
|
-
time_scope(PageView.order(created_at: :desc).limit(20).includes(visitor: :whitelisted_ip))
|
|
9
|
+
query = AnalyticsQuery.call(
|
|
10
|
+
scope: page_view_scope,
|
|
11
|
+
time_scope: apply_time_scope(page_view_scope),
|
|
12
|
+
limit: 10
|
|
19
13
|
)
|
|
20
14
|
|
|
21
15
|
render json: {
|
|
22
|
-
totals:
|
|
23
|
-
top_pages:
|
|
24
|
-
top_referrers:
|
|
25
|
-
top_sources:
|
|
26
|
-
recent:
|
|
27
|
-
{
|
|
28
|
-
path: pv.path,
|
|
29
|
-
ip: pv.visitor&.ip,
|
|
30
|
-
flagged_at: pv.visitor.flagged_at,
|
|
31
|
-
flagged_by: pv.visitor.flagged_by,
|
|
32
|
-
whitelisted: pv.visitor.whitelisted_ip&.active? || false,
|
|
33
|
-
user_agent: pv.user_agent,
|
|
34
|
-
session_id: pv.session_id,
|
|
35
|
-
trace_id: pv.trace_id,
|
|
36
|
-
referer: pv.referer,
|
|
37
|
-
source: pv.source,
|
|
38
|
-
created_at: pv.created_at
|
|
39
|
-
}
|
|
40
|
-
end
|
|
16
|
+
totals: query.totals,
|
|
17
|
+
top_pages: query.top_pages,
|
|
18
|
+
top_referrers: query.top_referrers,
|
|
19
|
+
top_sources: query.top_sources,
|
|
20
|
+
recent: visitor_filtered(apply_time_scope(query.recent)).map { |view| serialize_page_view(view) }
|
|
41
21
|
}
|
|
42
22
|
end
|
|
43
|
-
# rubocop:enable Metrics/AbcSize
|
|
44
23
|
|
|
45
24
|
private
|
|
46
25
|
|
|
@@ -50,7 +29,7 @@ module Trackguard
|
|
|
50
29
|
super
|
|
51
30
|
end
|
|
52
31
|
|
|
53
|
-
def
|
|
32
|
+
def apply_time_scope(base)
|
|
54
33
|
params[:since].present? ? base.where(created_at: parsed_since..) : base.last_30
|
|
55
34
|
end
|
|
56
35
|
|
|
@@ -64,8 +43,8 @@ module Trackguard
|
|
|
64
43
|
|
|
65
44
|
def visitor_filtered(scope)
|
|
66
45
|
if params.key?(:flagged)
|
|
67
|
-
|
|
68
|
-
scope = scope.joins(:visitor).merge(
|
|
46
|
+
flagged_scope = cast_bool(params[:flagged]) ? Visitor.flagged : Visitor.unflagged
|
|
47
|
+
scope = scope.joins(:visitor).merge(flagged_scope)
|
|
69
48
|
end
|
|
70
49
|
if params.key?(:whitelisted)
|
|
71
50
|
scope = if cast_bool(params[:whitelisted])
|
|
@@ -80,6 +59,22 @@ module Trackguard
|
|
|
80
59
|
def cast_bool(val)
|
|
81
60
|
ActiveRecord::Type::Boolean.new.cast(val)
|
|
82
61
|
end
|
|
62
|
+
|
|
63
|
+
def serialize_page_view(view)
|
|
64
|
+
{
|
|
65
|
+
path: view.path,
|
|
66
|
+
ip: view.visitor&.ip,
|
|
67
|
+
flagged_at: view.visitor.flagged_at,
|
|
68
|
+
flagged_by: view.visitor.flagged_by,
|
|
69
|
+
whitelisted: view.visitor.whitelisted_ip&.active? || false,
|
|
70
|
+
user_agent: view.user_agent,
|
|
71
|
+
session_id: view.session_id,
|
|
72
|
+
trace_id: view.trace_id,
|
|
73
|
+
referer: view.referer,
|
|
74
|
+
source: view.source,
|
|
75
|
+
created_at: view.created_at
|
|
76
|
+
}
|
|
77
|
+
end
|
|
83
78
|
end
|
|
84
79
|
end
|
|
85
80
|
end
|
|
@@ -12,7 +12,7 @@ module Trackguard
|
|
|
12
12
|
Rails.cache.delete(BlockedUserAgent::CACHE_KEY)
|
|
13
13
|
render json: { status: "ok", pattern: record.pattern }
|
|
14
14
|
rescue ActionController::ParameterMissing, ActiveRecord::RecordInvalid => e
|
|
15
|
-
render json: { status: "error", message: e.message }, status: :
|
|
15
|
+
render json: { status: "error", message: e.message }, status: :unprocessable_content
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
private
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
module Trackguard
|
|
2
2
|
module Admin
|
|
3
3
|
class DashboardsController < BaseController
|
|
4
|
-
|
|
5
|
-
@total_today = PageView.today.count
|
|
6
|
-
@total_week = PageView.this_week.count
|
|
7
|
-
@total_month = PageView.this_month.count
|
|
4
|
+
include Overridable
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
def show
|
|
7
|
+
query = AnalyticsQuery.call(
|
|
8
|
+
scope: page_view_scope,
|
|
9
|
+
time_scope: page_view_scope.last_30,
|
|
10
|
+
limit: 5
|
|
11
|
+
)
|
|
12
12
|
|
|
13
|
-
@
|
|
13
|
+
@total_today = query.totals[:today]
|
|
14
|
+
@total_week = query.totals[:week]
|
|
15
|
+
@total_month = query.totals[:month]
|
|
16
|
+
@top_pages = query.top_pages
|
|
17
|
+
@top_referrers = query.top_referrers
|
|
18
|
+
@top_sources = query.top_sources
|
|
19
|
+
@recent = query.recent
|
|
14
20
|
end
|
|
15
21
|
end
|
|
16
22
|
end
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
module Trackguard
|
|
2
2
|
module Admin
|
|
3
3
|
class VisitorsController < BaseController
|
|
4
|
+
include Overridable
|
|
5
|
+
|
|
4
6
|
skip_before_action :verify_authenticity_token, if: :valid_api_token?
|
|
5
7
|
before_action :set_visitor
|
|
6
8
|
|
|
7
9
|
rescue_from ActiveRecord::RecordNotFound do
|
|
8
10
|
respond_to do |format|
|
|
9
|
-
format.html { redirect_to
|
|
11
|
+
format.html { redirect_to after_action_path, alert: "Visitor not found." }
|
|
10
12
|
format.json { render json: { error: "Visitor not found" }, status: :not_found }
|
|
11
13
|
end
|
|
12
14
|
end
|
|
@@ -20,13 +22,13 @@ module Trackguard
|
|
|
20
22
|
name: params[:name].presence || BlockedUserAgent.matching_pattern(@visitor.user_agent)
|
|
21
23
|
)
|
|
22
24
|
respond_to do |format|
|
|
23
|
-
format.html { redirect_back_or_to
|
|
25
|
+
format.html { redirect_back_or_to after_action_path }
|
|
24
26
|
format.json { render json: { status: "ok", ip: @visitor.ip, flagged_at: @visitor.flagged_at } }
|
|
25
27
|
end
|
|
26
28
|
else
|
|
27
29
|
respond_to do |format|
|
|
28
|
-
format.html { redirect_back_or_to
|
|
29
|
-
format.json { render json: { errors: @visitor.errors.full_messages }, status: :
|
|
30
|
+
format.html { redirect_back_or_to after_action_path, alert: @visitor.errors.full_messages.join(", ") }
|
|
31
|
+
format.json { render json: { errors: @visitor.errors.full_messages }, status: :unprocessable_content }
|
|
30
32
|
end
|
|
31
33
|
end
|
|
32
34
|
end
|
|
@@ -35,7 +37,7 @@ module Trackguard
|
|
|
35
37
|
def unflag
|
|
36
38
|
@visitor.update!(flagged_at: nil, flag_reason: nil, flagged_by: nil)
|
|
37
39
|
respond_to do |format|
|
|
38
|
-
format.html { redirect_back_or_to
|
|
40
|
+
format.html { redirect_back_or_to after_action_path }
|
|
39
41
|
format.json { render json: { status: "ok", ip: @visitor.ip } }
|
|
40
42
|
end
|
|
41
43
|
end
|
|
@@ -47,14 +49,6 @@ module Trackguard
|
|
|
47
49
|
|
|
48
50
|
super
|
|
49
51
|
end
|
|
50
|
-
|
|
51
|
-
def set_visitor
|
|
52
|
-
@visitor = if params[:ip].present?
|
|
53
|
-
Visitor.find_by!(ip: params[:ip])
|
|
54
|
-
else
|
|
55
|
-
Visitor.find(params[:id])
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
52
|
end
|
|
59
53
|
end
|
|
60
54
|
end
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
module Trackguard
|
|
2
2
|
module Admin
|
|
3
3
|
class VisitsController < BaseController
|
|
4
|
+
include Overridable
|
|
5
|
+
|
|
4
6
|
PER_PAGE = 20
|
|
5
7
|
|
|
6
8
|
def index
|
|
7
9
|
@page = [ (params[:page] || 1).to_i, 1 ].max
|
|
8
|
-
@total =
|
|
10
|
+
@total = page_view_scope.count
|
|
9
11
|
@pages = (@total.to_f / PER_PAGE).ceil
|
|
10
|
-
@visits =
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
@visits = page_view_scope.order(created_at: :desc)
|
|
13
|
+
.limit(PER_PAGE)
|
|
14
|
+
.offset((@page - 1) * PER_PAGE)
|
|
15
|
+
.includes(visitor: :whitelisted_ip)
|
|
14
16
|
end
|
|
15
17
|
end
|
|
16
18
|
end
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
module Trackguard
|
|
2
2
|
module Admin
|
|
3
3
|
class WhitelistedIpsController < BaseController
|
|
4
|
+
include Overridable
|
|
5
|
+
|
|
4
6
|
skip_before_action :verify_authenticity_token, if: :valid_api_token?
|
|
5
7
|
before_action :set_visitor
|
|
6
8
|
|
|
7
9
|
rescue_from ActiveRecord::RecordNotFound do
|
|
8
10
|
respond_to do |format|
|
|
9
|
-
format.html { redirect_to
|
|
11
|
+
format.html { redirect_to after_action_path, alert: "Visitor not found." }
|
|
10
12
|
format.json { render json: { error: "Visitor not found" }, status: :not_found }
|
|
11
13
|
end
|
|
12
14
|
end
|
|
@@ -17,13 +19,13 @@ module Trackguard
|
|
|
17
19
|
record.expires_at = params[:expires_at].presence || 7.days.from_now
|
|
18
20
|
record.save!
|
|
19
21
|
respond_to do |format|
|
|
20
|
-
format.html { redirect_back_or_to
|
|
22
|
+
format.html { redirect_back_or_to after_action_path }
|
|
21
23
|
format.json { render json: { status: "ok", ip: @visitor.ip, expires_at: record.expires_at } }
|
|
22
24
|
end
|
|
23
25
|
rescue ActiveRecord::RecordInvalid => e
|
|
24
26
|
respond_to do |format|
|
|
25
|
-
format.html { redirect_back_or_to
|
|
26
|
-
format.json { render json: { status: "error", message: e.message }, status: :
|
|
27
|
+
format.html { redirect_back_or_to after_action_path, alert: e.message }
|
|
28
|
+
format.json { render json: { status: "error", message: e.message }, status: :unprocessable_content }
|
|
27
29
|
end
|
|
28
30
|
end
|
|
29
31
|
|
|
@@ -33,12 +35,12 @@ module Trackguard
|
|
|
33
35
|
if record
|
|
34
36
|
record.destroy!
|
|
35
37
|
respond_to do |format|
|
|
36
|
-
format.html { redirect_back_or_to
|
|
38
|
+
format.html { redirect_back_or_to after_action_path }
|
|
37
39
|
format.json { render json: { status: "ok", ip: @visitor.ip } }
|
|
38
40
|
end
|
|
39
41
|
else
|
|
40
42
|
respond_to do |format|
|
|
41
|
-
format.html { redirect_back_or_to
|
|
43
|
+
format.html { redirect_back_or_to after_action_path, alert: "No whitelist entry found." }
|
|
42
44
|
format.json { render json: { error: "Not whitelisted" }, status: :not_found }
|
|
43
45
|
end
|
|
44
46
|
end
|
|
@@ -51,14 +53,6 @@ module Trackguard
|
|
|
51
53
|
|
|
52
54
|
super
|
|
53
55
|
end
|
|
54
|
-
|
|
55
|
-
def set_visitor
|
|
56
|
-
@visitor = if params[:ip].present?
|
|
57
|
-
Visitor.find_by!(ip: params[:ip])
|
|
58
|
-
else
|
|
59
|
-
Visitor.find(params[:id])
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
56
|
end
|
|
63
57
|
end
|
|
64
58
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Trackguard
|
|
2
|
+
class AnalyticsQuery < ApplicationService
|
|
3
|
+
attr_reader :totals, :top_pages, :top_referrers, :top_sources, :recent
|
|
4
|
+
|
|
5
|
+
def initialize(scope:, time_scope:, limit:)
|
|
6
|
+
@scope = scope
|
|
7
|
+
@time_scope = time_scope
|
|
8
|
+
@limit = limit
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
@totals = {
|
|
13
|
+
today: @scope.today.count,
|
|
14
|
+
week: @scope.this_week.count,
|
|
15
|
+
month: @scope.this_month.count
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@top_pages = @time_scope.group(:path).order("count_all DESC").limit(@limit).count
|
|
19
|
+
@top_referrers = @time_scope.with_referrer.group(:referer).order("count_all DESC").limit(@limit).count
|
|
20
|
+
@top_sources = @time_scope.with_source.group(:source).order("count_all DESC").limit(@limit).count
|
|
21
|
+
|
|
22
|
+
@recent = @scope.order(created_at: :desc).limit(20).includes(visitor: :whitelisted_ip)
|
|
23
|
+
|
|
24
|
+
self
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
module Trackguard
|
|
2
2
|
class PageViewRecorder < ApplicationService
|
|
3
|
-
BOT_REGEX = /
|
|
4
|
-
Googlebot|Bingbot|Slurp|DuckDuckBot|Baidu|YandexBot|
|
|
5
|
-
facebookexternalhit|Twitterbot|LinkedInBot|
|
|
6
|
-
curl|wget|python-requests|python-urllib|
|
|
7
|
-
Go-http-client|libwww|Java|Ruby|
|
|
8
|
-
bot|crawl|spider
|
|
9
|
-
/ix
|
|
10
|
-
|
|
11
3
|
def initialize(path:, ip:, user_agent:, referer:, session_id:, trace_id:, source: nil, initial: false,
|
|
12
4
|
http_method: nil)
|
|
13
5
|
@path = path.to_s
|
|
@@ -22,11 +14,11 @@ module Trackguard
|
|
|
22
14
|
end
|
|
23
15
|
|
|
24
16
|
def call
|
|
25
|
-
|
|
26
|
-
return if
|
|
17
|
+
adapter = Trackguard.adapter
|
|
18
|
+
return if adapter.blocked_user_agent?(@user_agent)
|
|
27
19
|
return if @path.blank? || @path.start_with?("/admin")
|
|
28
20
|
|
|
29
|
-
|
|
21
|
+
adapter.track_page_view(
|
|
30
22
|
path: @path,
|
|
31
23
|
ip: @ip,
|
|
32
24
|
user_agent: @user_agent,
|
|
@@ -11,27 +11,7 @@
|
|
|
11
11
|
<div class="tg-container">
|
|
12
12
|
<div class="tg-header__inner">
|
|
13
13
|
<span class="tg-brand">
|
|
14
|
-
|
|
15
|
-
<defs>
|
|
16
|
-
<linearGradient id="tg-shield-grad" x1="0" y1="0" x2="0" y2="1">
|
|
17
|
-
<stop offset="0%" stop-color="#1e3a5f"/>
|
|
18
|
-
<stop offset="100%" stop-color="#0d1f3c"/>
|
|
19
|
-
</linearGradient>
|
|
20
|
-
</defs>
|
|
21
|
-
<!-- Shield -->
|
|
22
|
-
<path d="M10 1L19 4.5V11.5C19 16.4 15 20.3 10 21.5C5 20.3 1 16.4 1 11.5V4.5Z"
|
|
23
|
-
fill="url(#tg-shield-grad)" stroke="#3b82f6" stroke-width="1.25" stroke-linejoin="round"/>
|
|
24
|
-
<!-- Left rail -->
|
|
25
|
-
<line x1="7.5" y1="6" x2="7.5" y2="17" stroke="#60a5fa" stroke-width="1.6" stroke-linecap="round"/>
|
|
26
|
-
<!-- Right rail -->
|
|
27
|
-
<line x1="12.5" y1="6" x2="12.5" y2="17" stroke="#60a5fa" stroke-width="1.6" stroke-linecap="round"/>
|
|
28
|
-
<!-- Cross-tie 1 -->
|
|
29
|
-
<line x1="6.25" y1="8" x2="13.75" y2="8" stroke="#3b82f6" stroke-width="1.25" stroke-linecap="round"/>
|
|
30
|
-
<!-- Cross-tie 2 -->
|
|
31
|
-
<line x1="6.25" y1="11.5" x2="13.75" y2="11.5" stroke="#3b82f6" stroke-width="1.25" stroke-linecap="round"/>
|
|
32
|
-
<!-- Cross-tie 3 -->
|
|
33
|
-
<line x1="6.25" y1="15" x2="13.75" y2="15" stroke="#3b82f6" stroke-width="1.25" stroke-linecap="round"/>
|
|
34
|
-
</svg>
|
|
14
|
+
<%= image_tag "trackguard/logo.png", class: "tg-brand__logo", alt: "Trackguard" %>
|
|
35
15
|
Trackguard
|
|
36
16
|
</span>
|
|
37
17
|
|
|
@@ -2,6 +2,7 @@ class CreateTrackguardTables < ActiveRecord::Migration[<%= ActiveRecord::Migrati
|
|
|
2
2
|
def change
|
|
3
3
|
create_table :trackguard_visitors do |t|
|
|
4
4
|
t.string :ip
|
|
5
|
+
t.string :name
|
|
5
6
|
t.string :user_agent
|
|
6
7
|
t.datetime :first_seen_at, null: false
|
|
7
8
|
t.datetime :last_seen_at, null: false
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Trackguard
|
|
4
|
+
module Adapters
|
|
5
|
+
class Base
|
|
6
|
+
def blocked_user_agent?(user_agent) = raise NotImplementedError, "#{self.class}#blocked_user_agent?"
|
|
7
|
+
def whitelisted_ip?(ip) = raise NotImplementedError, "#{self.class}#whitelisted_ip?"
|
|
8
|
+
def flagged_visitor?(ip) = raise NotImplementedError, "#{self.class}#flagged_visitor?"
|
|
9
|
+
|
|
10
|
+
def track_page_view(path:, ip:, user_agent:, referer:, session_id:, trace_id:, source:, initial:, http_method:)
|
|
11
|
+
raise NotImplementedError, "#{self.class}#track_page_view"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def track_blocked_request(ip:, user_agent:, path:, http_method:, block_reason:)
|
|
15
|
+
raise NotImplementedError, "#{self.class}#track_blocked_request"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Trackguard
|
|
4
|
+
module Adapters
|
|
5
|
+
class Local < Base
|
|
6
|
+
def blocked_user_agent?(user_agent)
|
|
7
|
+
BlockedUserAgent.blocked?(user_agent)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def whitelisted_ip?(ip)
|
|
11
|
+
WhitelistedIp.whitelisted?(ip)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def flagged_visitor?(ip)
|
|
15
|
+
Visitor.flagged?(ip)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def track_page_view(path:, ip:, user_agent:, referer:, session_id:, trace_id:, source:, initial:, http_method:)
|
|
19
|
+
TrackPageViewJob.perform_later(
|
|
20
|
+
path: path,
|
|
21
|
+
ip: ip,
|
|
22
|
+
user_agent: user_agent,
|
|
23
|
+
referer: referer,
|
|
24
|
+
session_id: session_id,
|
|
25
|
+
trace_id: trace_id,
|
|
26
|
+
source: source,
|
|
27
|
+
initial: initial,
|
|
28
|
+
http_method: http_method
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def track_blocked_request(ip:, user_agent:, path:, http_method:, block_reason:)
|
|
33
|
+
TrackBlockedRequestJob.perform_later(
|
|
34
|
+
ip: ip,
|
|
35
|
+
user_agent: user_agent,
|
|
36
|
+
path: path,
|
|
37
|
+
http_method: http_method,
|
|
38
|
+
block_reason: block_reason
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -5,20 +5,22 @@ require "rack/attack"
|
|
|
5
5
|
module Trackguard
|
|
6
6
|
module RackAttack
|
|
7
7
|
def self.configure
|
|
8
|
+
adapter = Trackguard.adapter
|
|
9
|
+
|
|
8
10
|
::Rack::Attack.safelist("trackguard/allow local") do |req|
|
|
9
11
|
[ "127.0.0.1", "::1" ].include?(req.ip)
|
|
10
12
|
end
|
|
11
13
|
|
|
12
14
|
::Rack::Attack.safelist("trackguard/allow whitelisted ips") do |req|
|
|
13
|
-
|
|
15
|
+
adapter.whitelisted_ip?(req.ip)
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
::Rack::Attack.blocklist("trackguard/block known scanners") do |req|
|
|
17
|
-
|
|
19
|
+
adapter.blocked_user_agent?(req.user_agent)
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
::Rack::Attack.blocklist("trackguard/flagged visitors") do |req|
|
|
21
|
-
|
|
23
|
+
adapter.flagged_visitor?(req.ip)
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
::Rack::Attack.throttle(
|
|
@@ -27,15 +29,15 @@ module Trackguard
|
|
|
27
29
|
period: Trackguard.throttle_period, &:ip
|
|
28
30
|
)
|
|
29
31
|
|
|
30
|
-
subscribe_to_blocked_requests
|
|
32
|
+
subscribe_to_blocked_requests(adapter)
|
|
31
33
|
end
|
|
32
34
|
|
|
33
|
-
def self.subscribe_to_blocked_requests
|
|
35
|
+
def self.subscribe_to_blocked_requests(adapter)
|
|
34
36
|
@subscribe_to_blocked_requests ||= ActiveSupport::Notifications.subscribe("rack.attack") do |*, payload|
|
|
35
37
|
req = payload[:request]
|
|
36
38
|
next unless req.env["rack.attack.match_type"] == :blocklist
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
adapter.track_blocked_request(
|
|
39
41
|
ip: req.ip,
|
|
40
42
|
user_agent: req.user_agent.to_s,
|
|
41
43
|
path: req.path,
|
data/lib/trackguard/version.rb
CHANGED
data/lib/trackguard.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require "trackguard/version"
|
|
4
4
|
require "trackguard/engine"
|
|
5
5
|
require "trackguard/rack_attack"
|
|
6
|
+
require "trackguard/adapters/base"
|
|
7
|
+
require "trackguard/adapters/local"
|
|
6
8
|
|
|
7
9
|
module Trackguard
|
|
8
10
|
class << self
|
|
@@ -36,5 +38,23 @@ module Trackguard
|
|
|
36
38
|
def throttle_period
|
|
37
39
|
@throttle_period ||= 60
|
|
38
40
|
end
|
|
41
|
+
|
|
42
|
+
def adapter
|
|
43
|
+
@adapter ||= Trackguard::Adapters::Local.new
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def adapter=(value)
|
|
47
|
+
@adapter = resolve_adapter(value)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def resolve_adapter(value)
|
|
53
|
+
case value
|
|
54
|
+
when Symbol then Trackguard::Adapters.const_get(value.to_s.camelize).new
|
|
55
|
+
when Class then value.new
|
|
56
|
+
else value
|
|
57
|
+
end
|
|
58
|
+
end
|
|
39
59
|
end
|
|
40
60
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: trackguard
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.20.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Krzysztof Rygielski
|
|
@@ -43,8 +43,10 @@ executables: []
|
|
|
43
43
|
extensions: []
|
|
44
44
|
extra_rdoc_files: []
|
|
45
45
|
files:
|
|
46
|
+
- app/assets/images/trackguard/logo.png
|
|
46
47
|
- app/assets/javascripts/controllers/page_tracker_controller.js
|
|
47
48
|
- app/assets/stylesheets/trackguard/admin.css
|
|
49
|
+
- app/controllers/concerns/trackguard/admin/overridable.rb
|
|
48
50
|
- app/controllers/concerns/trackguard/page_tracker.rb
|
|
49
51
|
- app/controllers/trackguard/admin/analytics_controller.rb
|
|
50
52
|
- app/controllers/trackguard/admin/base_controller.rb
|
|
@@ -64,6 +66,7 @@ files:
|
|
|
64
66
|
- app/models/trackguard/visit.rb
|
|
65
67
|
- app/models/trackguard/visitor.rb
|
|
66
68
|
- app/models/trackguard/whitelisted_ip.rb
|
|
69
|
+
- app/services/trackguard/analytics_query.rb
|
|
67
70
|
- app/services/trackguard/application_service.rb
|
|
68
71
|
- app/services/trackguard/page_view_recorder.rb
|
|
69
72
|
- app/views/layouts/trackguard/admin.html.erb
|
|
@@ -80,6 +83,8 @@ files:
|
|
|
80
83
|
- lib/generators/trackguard/upgrade_generator.rb
|
|
81
84
|
- lib/tasks/trackguard.rake
|
|
82
85
|
- lib/trackguard.rb
|
|
86
|
+
- lib/trackguard/adapters/base.rb
|
|
87
|
+
- lib/trackguard/adapters/local.rb
|
|
83
88
|
- lib/trackguard/engine.rb
|
|
84
89
|
- lib/trackguard/rack_attack.rb
|
|
85
90
|
- lib/trackguard/version.rb
|