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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79ebac148a49b2b6df9f280c3a8e8e624fa82910ccf0e3d4c608a04ac7f73ced
4
- data.tar.gz: a0a0648dacb35df737e19413603d658b1c015f6e5fa60d0a6adc72da497082da
3
+ metadata.gz: 3e13b68029bc80d015c4f69531d17e5b362092d6df2d720dfb7c1563b5b4e24c
4
+ data.tar.gz: e676a88de14a04d1c42c0a78f7e895a7ef5011c7567e93a168e82e8f950bfeb1
5
5
  SHA512:
6
- metadata.gz: 59d2f2a7dde041da4cc0acf29249c77d9f911a76c498f9381293c838a29bd6ac5fec3c875645c7be60b686d77cb99e4731032b45eb7a2a93464dadaac81b04cb
7
- data.tar.gz: 5c1bf529774f1af5e8a7e83154e7e21b94e468c95702ac00bed7f0b133b85c092359e544e7f2f615d4e364d904e4512d16ad42d42e3c712d8f8dcb4b60cb81ae
6
+ metadata.gz: d4074d349ac565380d3365ef21ca99e61836601f51fe5822cff34a3184f93cea71dc0ab541b2a08f2f0ccb49fade36c89a97f8681a5609748088ecdb661f6f84
7
+ data.tar.gz: 1783d02fa8caedd5816efce4cca6faab7c305af260549de8ece369c22ff47b279725e9503dc5c3d8cade1f5069516faaee594fd362e16b92939317168f4d7dfc
@@ -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: #07101f;
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: #0d1829;
26
- border-bottom: 1px solid #1c2d4a;
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: #60a5fa;
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: 22px;
50
+ height: auto;
51
51
  flex-shrink: 0;
52
- filter: drop-shadow(0 0 4px rgba(59, 130, 246, 0.35));
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: #0d1829;
80
- border-bottom: 1px solid #1c2d4a;
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: #60a5fa;
101
- border-bottom-color: #60a5fa;
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: #0d1829;
132
- border: 1px solid #1c2d4a;
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: #0d1829;
171
- border: 1px solid #1c2d4a;
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: #1c2d4a;
214
+ background: #2e1515;
215
215
  }
216
216
 
217
- .tg-pagination__link:hover { background: #243a5e; }
217
+ .tg-pagination__link:hover { background: #3d1c1c; }
218
218
 
219
219
  .tg-pagination__link--active {
220
- background: #60a5fa;
221
- color: #07101f;
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 #1c2d4a;
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 #1c2d4a;
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 #1c2d4a;
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: #0d1829;
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 #1c2d4a;
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: #07101f;
440
- border: 1px solid #1c2d4a;
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: #60a5fa; }
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: #1c2d4a; color: #e0e0e0; }
465
- .tg-btn--ghost:hover { background: #243a5e; }
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
- @total_today = PageView.today.count
9
- @total_week = PageView.this_week.count
10
- @total_month = PageView.this_month.count
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: { today: @total_today, week: @total_week, month: @total_month },
23
- top_pages: @top_pages,
24
- top_referrers: @top_referrers,
25
- top_sources: @top_sources,
26
- recent: @recent.map do |pv|
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 time_scope(base = PageView.all)
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
- visitor_scope = cast_bool(params[:flagged]) ? Visitor.flagged : Visitor.unflagged
68
- scope = scope.joins(:visitor).merge(visitor_scope)
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: :unprocessable_entity
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
- def show
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
- @top_pages = PageView.last_30.group(:path).order("count_all DESC").limit(5).count
10
- @top_referrers = PageView.last_30.with_referrer.group(:referer).order("count_all DESC").limit(5).count
11
- @top_sources = PageView.last_30.with_source.group(:source).order("count_all DESC").limit(5).count
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
- @recent = PageView.order(created_at: :desc).limit(20).includes(visitor: :whitelisted_ip)
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 dashboard_path, alert: "Visitor not found." }
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 dashboard_path }
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 dashboard_path, alert: @visitor.errors.full_messages.join(", ") }
29
- format.json { render json: { errors: @visitor.errors.full_messages }, status: :unprocessable_entity }
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 dashboard_path }
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 = PageView.count
10
+ @total = page_view_scope.count
9
11
  @pages = (@total.to_f / PER_PAGE).ceil
10
- @visits = PageView.order(created_at: :desc)
11
- .limit(PER_PAGE)
12
- .offset((@page - 1) * PER_PAGE)
13
- .includes(visitor: :whitelisted_ip)
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 dashboard_path, alert: "Visitor not found." }
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 dashboard_path }
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 dashboard_path, alert: e.message }
26
- format.json { render json: { status: "error", message: e.message }, status: :unprocessable_entity }
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 dashboard_path }
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 dashboard_path, alert: "No whitelist entry found." }
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
- return if BOT_REGEX.match?(@user_agent)
26
- return if BlockedUserAgent.blocked?(@user_agent)
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
- TrackPageViewJob.perform_later(
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
- <svg class="tg-brand__logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 22" aria-hidden="true">
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
- Trackguard::WhitelistedIp.whitelisted?(req.ip)
15
+ adapter.whitelisted_ip?(req.ip)
14
16
  end
15
17
 
16
18
  ::Rack::Attack.blocklist("trackguard/block known scanners") do |req|
17
- Trackguard::BlockedUserAgent.blocked?(req.user_agent)
19
+ adapter.blocked_user_agent?(req.user_agent)
18
20
  end
19
21
 
20
22
  ::Rack::Attack.blocklist("trackguard/flagged visitors") do |req|
21
- Trackguard::Visitor.flagged?(req.ip)
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
- Trackguard::TrackBlockedRequestJob.perform_later(
40
+ adapter.track_blocked_request(
39
41
  ip: req.ip,
40
42
  user_agent: req.user_agent.to_s,
41
43
  path: req.path,
@@ -1,3 +1,3 @@
1
1
  module Trackguard
2
- VERSION = "0.17.0".freeze
2
+ VERSION = "0.20.0".freeze
3
3
  end
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.17.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