beskar 0.0.2 → 0.1.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/CHANGELOG.md +143 -0
- data/README.md +298 -110
- data/app/controllers/beskar/application_controller.rb +170 -0
- data/app/controllers/beskar/banned_ips_controller.rb +280 -0
- data/app/controllers/beskar/dashboard_controller.rb +70 -0
- data/app/controllers/beskar/security_events_controller.rb +182 -0
- data/app/controllers/concerns/beskar/controllers/security_tracking.rb +6 -6
- data/app/models/beskar/banned_ip.rb +68 -27
- data/app/models/beskar/security_event.rb +14 -0
- data/app/services/beskar/banned_ip_manager.rb +78 -0
- data/app/views/beskar/banned_ips/edit.html.erb +259 -0
- data/app/views/beskar/banned_ips/index.html.erb +361 -0
- data/app/views/beskar/banned_ips/new.html.erb +310 -0
- data/app/views/beskar/banned_ips/show.html.erb +310 -0
- data/app/views/beskar/dashboard/index.html.erb +280 -0
- data/app/views/beskar/security_events/index.html.erb +309 -0
- data/app/views/beskar/security_events/show.html.erb +307 -0
- data/app/views/layouts/beskar/application.html.erb +647 -5
- data/config/routes.rb +41 -0
- data/lib/beskar/configuration.rb +24 -10
- data/lib/beskar/engine.rb +4 -4
- data/lib/beskar/logger.rb +293 -0
- data/lib/beskar/middleware/request_analyzer.rb +128 -53
- data/lib/beskar/models/security_trackable_authenticable.rb +11 -11
- data/lib/beskar/models/security_trackable_devise.rb +5 -5
- data/lib/beskar/models/security_trackable_generic.rb +12 -12
- data/lib/beskar/services/account_locker.rb +12 -12
- data/lib/beskar/services/geolocation_service.rb +8 -8
- data/lib/beskar/services/ip_whitelist.rb +2 -2
- data/lib/beskar/services/waf.rb +307 -78
- data/lib/beskar/version.rb +1 -1
- data/lib/beskar.rb +1 -0
- data/lib/generators/beskar/install/install_generator.rb +158 -0
- data/lib/generators/beskar/install/templates/initializer.rb.tt +177 -0
- data/lib/tasks/beskar_tasks.rake +11 -2
- metadata +35 -6
- data/lib/beskar/templates/beskar_initializer.rb +0 -107
|
@@ -29,9 +29,9 @@ module Beskar
|
|
|
29
29
|
return unless Beskar.configuration.track_successful_logins?
|
|
30
30
|
|
|
31
31
|
user.track_authentication_event(request, :success)
|
|
32
|
-
|
|
32
|
+
Beskar::Logger.info("Tracked successful authentication for user #{user.id}")
|
|
33
33
|
rescue => e
|
|
34
|
-
|
|
34
|
+
Beskar::Logger.error("Failed to track authentication success: #{e.message}")
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# Track failed authentication attempt
|
|
@@ -40,9 +40,9 @@ module Beskar
|
|
|
40
40
|
return unless Beskar.configuration.track_failed_logins?
|
|
41
41
|
|
|
42
42
|
model_class.track_failed_authentication(request, scope)
|
|
43
|
-
|
|
43
|
+
Beskar::Logger.info("Tracked failed authentication for scope #{scope}")
|
|
44
44
|
rescue => e
|
|
45
|
-
|
|
45
|
+
Beskar::Logger.error("Failed to track authentication failure: #{e.message}")
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
# Track logout event
|
|
@@ -61,9 +61,9 @@ module Beskar
|
|
|
61
61
|
},
|
|
62
62
|
risk_score: 0
|
|
63
63
|
)
|
|
64
|
-
|
|
64
|
+
Beskar::Logger.info("Tracked logout for user #{user.id}")
|
|
65
65
|
rescue => e
|
|
66
|
-
|
|
66
|
+
Beskar::Logger.error("Failed to track logout: #{e.message}")
|
|
67
67
|
end
|
|
68
68
|
end
|
|
69
69
|
end
|
|
@@ -12,6 +12,10 @@ module Beskar
|
|
|
12
12
|
self.metadata ||= {}
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
# Cache management callbacks
|
|
16
|
+
after_save :update_cache
|
|
17
|
+
after_destroy :clear_cache
|
|
18
|
+
|
|
15
19
|
scope :active, -> { where("expires_at IS NULL OR expires_at > ?", Time.current) }
|
|
16
20
|
scope :permanent, -> { where(permanent: true) }
|
|
17
21
|
scope :temporary, -> { where(permanent: false) }
|
|
@@ -67,35 +71,52 @@ module Beskar
|
|
|
67
71
|
class << self
|
|
68
72
|
# Ban an IP address
|
|
69
73
|
def ban!(ip_address, reason:, duration: nil, permanent: false, details: nil, metadata: {})
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
banned_ip
|
|
76
|
-
|
|
77
|
-
if
|
|
78
|
-
|
|
74
|
+
# Retry logic to handle race conditions when multiple requests try to ban the same IP
|
|
75
|
+
retries = 0
|
|
76
|
+
max_retries = 2
|
|
77
|
+
|
|
78
|
+
begin
|
|
79
|
+
banned_ip = find_or_initialize_by(ip_address: ip_address)
|
|
80
|
+
|
|
81
|
+
if banned_ip.persisted?
|
|
82
|
+
# Existing ban - extend it
|
|
83
|
+
banned_ip.extend_ban!(duration)
|
|
84
|
+
banned_ip.details = details if details
|
|
85
|
+
# Deep stringify keys to avoid duplicate key issues
|
|
86
|
+
if metadata.any?
|
|
87
|
+
banned_ip.metadata = banned_ip.metadata.deep_stringify_keys.merge(metadata.deep_stringify_keys)
|
|
88
|
+
end
|
|
89
|
+
banned_ip.save!
|
|
90
|
+
else
|
|
91
|
+
# New ban
|
|
92
|
+
banned_ip.assign_attributes(
|
|
93
|
+
reason: reason,
|
|
94
|
+
banned_at: Time.current,
|
|
95
|
+
expires_at: permanent ? nil : (Time.current + (duration || 1.hour)),
|
|
96
|
+
permanent: permanent,
|
|
97
|
+
details: details,
|
|
98
|
+
metadata: metadata
|
|
99
|
+
)
|
|
100
|
+
banned_ip.save!
|
|
79
101
|
end
|
|
80
|
-
banned_ip.save!
|
|
81
|
-
else
|
|
82
|
-
# New ban
|
|
83
|
-
banned_ip.assign_attributes(
|
|
84
|
-
reason: reason,
|
|
85
|
-
banned_at: Time.current,
|
|
86
|
-
expires_at: permanent ? nil : (Time.current + (duration || 1.hour)),
|
|
87
|
-
permanent: permanent,
|
|
88
|
-
details: details,
|
|
89
|
-
metadata: metadata
|
|
90
|
-
)
|
|
91
|
-
banned_ip.save!
|
|
92
|
-
end
|
|
93
102
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
103
|
+
# Update cache
|
|
104
|
+
cache_key = "beskar:banned_ip:#{ip_address}"
|
|
105
|
+
Rails.cache.write(cache_key, true, expires_in: permanent ? nil : (duration || 1.hour))
|
|
106
|
+
|
|
107
|
+
banned_ip
|
|
108
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
109
|
+
# Race condition: another request created the record between find and save
|
|
110
|
+
if e.message.include?("Ip address has already been taken") && retries < max_retries
|
|
111
|
+
retries += 1
|
|
112
|
+
# Small random delay to reduce contention (1-10ms)
|
|
113
|
+
sleep(rand(1..10) / 1000.0)
|
|
114
|
+
retry
|
|
115
|
+
else
|
|
116
|
+
# Re-raise if it's a different validation error or we've exceeded retries
|
|
117
|
+
raise
|
|
118
|
+
end
|
|
119
|
+
end
|
|
99
120
|
end
|
|
100
121
|
|
|
101
122
|
# Check if an IP is banned (cache-first approach)
|
|
@@ -148,5 +169,25 @@ module Beskar
|
|
|
148
169
|
expired.destroy_all
|
|
149
170
|
end
|
|
150
171
|
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def update_cache
|
|
176
|
+
return unless active?
|
|
177
|
+
|
|
178
|
+
cache_key = "beskar:banned_ip:#{ip_address}"
|
|
179
|
+
ttl = calculate_cache_ttl
|
|
180
|
+
Rails.cache.write(cache_key, true, expires_in: ttl)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def clear_cache
|
|
184
|
+
Rails.cache.delete("beskar:banned_ip:#{ip_address}")
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def calculate_cache_ttl
|
|
188
|
+
return nil if permanent? || expires_at.nil?
|
|
189
|
+
|
|
190
|
+
(expires_at - Time.current).to_i
|
|
191
|
+
end
|
|
151
192
|
end
|
|
152
193
|
end
|
|
@@ -46,5 +46,19 @@ module Beskar
|
|
|
46
46
|
def geolocation
|
|
47
47
|
metadata&.dig("geolocation") || {}
|
|
48
48
|
end
|
|
49
|
+
|
|
50
|
+
def details
|
|
51
|
+
# Extract details from metadata if available
|
|
52
|
+
# Check multiple possible fields where details might be stored
|
|
53
|
+
return nil unless metadata.present?
|
|
54
|
+
|
|
55
|
+
metadata["details"] ||
|
|
56
|
+
metadata["description"] ||
|
|
57
|
+
metadata["message"] ||
|
|
58
|
+
metadata["reason"] ||
|
|
59
|
+
metadata["error"] ||
|
|
60
|
+
metadata["info"] ||
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
49
63
|
end
|
|
50
64
|
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Beskar
|
|
2
|
+
class BannedIpManager
|
|
3
|
+
attr_reader :banned_ip, :errors
|
|
4
|
+
|
|
5
|
+
def initialize(params)
|
|
6
|
+
@params = params
|
|
7
|
+
@errors = []
|
|
8
|
+
@banned_ip = nil
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def create
|
|
12
|
+
@banned_ip = BannedIp.new(base_attributes)
|
|
13
|
+
configure_ban_duration
|
|
14
|
+
|
|
15
|
+
@banned_ip.save
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def success?
|
|
19
|
+
@banned_ip&.persisted?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def base_attributes
|
|
25
|
+
{
|
|
26
|
+
ip_address: @params[:ip_address],
|
|
27
|
+
reason: @params[:reason],
|
|
28
|
+
details: @params[:details],
|
|
29
|
+
violation_count: @params[:violation_count] || 1,
|
|
30
|
+
metadata: @params[:metadata] || {},
|
|
31
|
+
banned_at: Time.current
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def configure_ban_duration
|
|
36
|
+
return set_permanent_ban if permanent_ban?
|
|
37
|
+
|
|
38
|
+
set_temporary_ban
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def permanent_ban?
|
|
42
|
+
@params[:ban_type] == 'permanent'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def set_permanent_ban
|
|
46
|
+
@banned_ip.permanent = true
|
|
47
|
+
@banned_ip.expires_at = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def set_temporary_ban
|
|
51
|
+
@banned_ip.permanent = false
|
|
52
|
+
@banned_ip.expires_at = calculate_expiry_time
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def calculate_expiry_time
|
|
56
|
+
return custom_expiry_time if custom_expiry_time.present?
|
|
57
|
+
return preset_duration_expiry if preset_duration.present?
|
|
58
|
+
|
|
59
|
+
default_expiry_time
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def custom_expiry_time
|
|
63
|
+
@params[:expires_at]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def preset_duration
|
|
67
|
+
@params[:duration]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def preset_duration_expiry
|
|
71
|
+
Time.current + preset_duration.to_i.seconds
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def default_expiry_time
|
|
75
|
+
24.hours.from_now
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
<% content_for :page_title, "Edit Ban: #{@banned_ip.ip_address}" %>
|
|
2
|
+
|
|
3
|
+
<% content_for :header_actions do %>
|
|
4
|
+
<%= link_to "← Cancel", banned_ip_path(@banned_ip), class: "btn btn-secondary" %>
|
|
5
|
+
<% end %>
|
|
6
|
+
|
|
7
|
+
<div class="card">
|
|
8
|
+
<div class="card-header">
|
|
9
|
+
<h3 class="card-title">Edit IP Ban</h3>
|
|
10
|
+
<div class="card-subtitle">Modify ban settings for <%= @banned_ip.ip_address %></div>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="card-body">
|
|
13
|
+
<%= form_with model: @banned_ip, url: banned_ip_path(@banned_ip), method: :patch, local: true do |f| %>
|
|
14
|
+
<% if @banned_ip.errors.any? %>
|
|
15
|
+
<div class="alert alert-danger">
|
|
16
|
+
<h4 style="font-size: 14px; font-weight: 600; margin-bottom: 0.5rem;">
|
|
17
|
+
<%= pluralize(@banned_ip.errors.count, "error") %> prohibited this ban from being saved:
|
|
18
|
+
</h4>
|
|
19
|
+
<ul style="margin: 0; padding-left: 1.5rem;">
|
|
20
|
+
<% @banned_ip.errors.full_messages.each do |message| %>
|
|
21
|
+
<li style="font-size: 13px;"><%= message %></li>
|
|
22
|
+
<% end %>
|
|
23
|
+
</ul>
|
|
24
|
+
</div>
|
|
25
|
+
<% end %>
|
|
26
|
+
|
|
27
|
+
<div style="display: grid; grid-template-columns: 1fr; gap: 1.5rem; max-width: 600px;">
|
|
28
|
+
<!-- IP Address (Read-only) -->
|
|
29
|
+
<div class="form-group">
|
|
30
|
+
<%= f.label :ip_address, "IP Address" %>
|
|
31
|
+
<%= f.text_field :ip_address,
|
|
32
|
+
disabled: true,
|
|
33
|
+
style: "font-family: monospace; background: #F4F5F7; cursor: not-allowed;" %>
|
|
34
|
+
<div style="font-size: 12px; color: #697386; margin-top: 0.25rem;">
|
|
35
|
+
IP addresses cannot be changed. To ban a different IP, create a new ban.
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<!-- Current Status -->
|
|
40
|
+
<div class="form-group">
|
|
41
|
+
<label>Current Status</label>
|
|
42
|
+
<div style="display: flex; align-items: center; gap: 1rem;">
|
|
43
|
+
<% if @banned_ip.permanent? %>
|
|
44
|
+
<span class="badge badge-critical">Permanent Ban</span>
|
|
45
|
+
<% elsif @banned_ip.active? %>
|
|
46
|
+
<span class="badge badge-danger">Active</span>
|
|
47
|
+
<span style="font-size: 13px; color: #697386;">
|
|
48
|
+
Expires in <%= distance_of_time_in_words(Time.current, @banned_ip.expires_at) %>
|
|
49
|
+
</span>
|
|
50
|
+
<% else %>
|
|
51
|
+
<span class="badge badge-neutral">Expired</span>
|
|
52
|
+
<span style="font-size: 13px; color: #697386;">
|
|
53
|
+
Expired <%= time_ago_in_words(@banned_ip.expires_at) %> ago
|
|
54
|
+
</span>
|
|
55
|
+
<% end %>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<!-- Reason -->
|
|
60
|
+
<div class="form-group">
|
|
61
|
+
<%= f.label :reason, "Ban Reason *" %>
|
|
62
|
+
<%= f.select :reason,
|
|
63
|
+
options_for_select([
|
|
64
|
+
['Rate Limit Abuse', 'rate_limit_abuse'],
|
|
65
|
+
['Authentication Abuse', 'authentication_abuse'],
|
|
66
|
+
['WAF Violation', 'waf_violation'],
|
|
67
|
+
['Brute Force Attack', 'brute_force_attack'],
|
|
68
|
+
['Suspicious Activity', 'suspicious_activity'],
|
|
69
|
+
['Manual Ban', 'manual_ban'],
|
|
70
|
+
['Other', 'other']
|
|
71
|
+
], @banned_ip.reason),
|
|
72
|
+
{ prompt: 'Select a reason...' },
|
|
73
|
+
{ required: true } %>
|
|
74
|
+
<div style="font-size: 12px; color: #697386; margin-top: 0.25rem;">
|
|
75
|
+
Update the reason for this ban if needed.
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<!-- Ban Duration -->
|
|
80
|
+
<div class="form-group">
|
|
81
|
+
<label>Ban Duration</label>
|
|
82
|
+
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
|
83
|
+
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
|
84
|
+
<%= f.radio_button :permanent, false,
|
|
85
|
+
onchange: "toggleDurationFields()",
|
|
86
|
+
checked: !@banned_ip.permanent? %>
|
|
87
|
+
<span style="font-weight: normal;">Temporary Ban</span>
|
|
88
|
+
</label>
|
|
89
|
+
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
|
90
|
+
<%= f.radio_button :permanent, true,
|
|
91
|
+
onchange: "toggleDurationFields()",
|
|
92
|
+
checked: @banned_ip.permanent? %>
|
|
93
|
+
<span style="font-weight: normal; color: #D32F2F;">Permanent Ban</span>
|
|
94
|
+
</label>
|
|
95
|
+
</div>
|
|
96
|
+
<% if @banned_ip.permanent? %>
|
|
97
|
+
<div class="alert alert-warning" style="margin-top: 0.5rem;">
|
|
98
|
+
<strong>Warning:</strong> This is currently a permanent ban. Changing it to temporary will set an expiration date.
|
|
99
|
+
</div>
|
|
100
|
+
<% end %>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- Temporary Ban Options -->
|
|
104
|
+
<div id="temporary-options" style="<%= @banned_ip.permanent? ? 'display: none;' : '' %>">
|
|
105
|
+
<div class="form-group">
|
|
106
|
+
<%= f.label :expires_at, "Expiry Date/Time" %>
|
|
107
|
+
<%= f.datetime_field :expires_at,
|
|
108
|
+
value: @banned_ip.expires_at&.strftime('%Y-%m-%dT%H:%M'),
|
|
109
|
+
min: DateTime.now.strftime('%Y-%m-%dT%H:%M') %>
|
|
110
|
+
<div style="font-size: 12px; color: #697386; margin-top: 0.25rem;">
|
|
111
|
+
Set when this ban should expire. Leave empty for default duration.
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<!-- Quick Extension Options -->
|
|
116
|
+
<div class="form-group">
|
|
117
|
+
<label>Quick Extension</label>
|
|
118
|
+
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem;">
|
|
119
|
+
<button type="button" onclick="extendBan(1)" class="btn btn-sm btn-secondary">+1 Hour</button>
|
|
120
|
+
<button type="button" onclick="extendBan(24)" class="btn btn-sm btn-secondary">+1 Day</button>
|
|
121
|
+
<button type="button" onclick="extendBan(168)" class="btn btn-sm btn-secondary">+7 Days</button>
|
|
122
|
+
<button type="button" onclick="extendBan(720)" class="btn btn-sm btn-secondary">+30 Days</button>
|
|
123
|
+
<button type="button" onclick="extendBan(2160)" class="btn btn-sm btn-secondary">+90 Days</button>
|
|
124
|
+
<button type="button" onclick="makePermanent()" class="btn btn-sm btn-danger">Make Permanent</button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<!-- Details -->
|
|
130
|
+
<div class="form-group">
|
|
131
|
+
<%= f.label :details, "Additional Details" %>
|
|
132
|
+
<%= f.text_area :details,
|
|
133
|
+
rows: 3,
|
|
134
|
+
placeholder: "Provide any additional context or details about this ban..." %>
|
|
135
|
+
<div style="font-size: 12px; color: #697386; margin-top: 0.25rem;">
|
|
136
|
+
Update any relevant information about why this IP is banned.
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<!-- Violation Count -->
|
|
141
|
+
<div class="form-group">
|
|
142
|
+
<%= f.label :violation_count, "Violation Count" %>
|
|
143
|
+
<%= f.number_field :violation_count, min: 0 %>
|
|
144
|
+
<div style="font-size: 12px; color: #697386; margin-top: 0.25rem;">
|
|
145
|
+
Current violation count: <%= @banned_ip.violation_count %>. Higher counts may result in longer bans for repeat offenders.
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<hr style="border: none; border-top: 1px solid #E3E8EE; margin: 1.5rem 0;">
|
|
150
|
+
|
|
151
|
+
<!-- Ban History -->
|
|
152
|
+
<div class="form-group">
|
|
153
|
+
<label>Ban History</label>
|
|
154
|
+
<div style="background: #FAFBFC; border: 1px solid #E3E8EE; border-radius: 6px; padding: 1rem;">
|
|
155
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem;">
|
|
156
|
+
<div>
|
|
157
|
+
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386;">
|
|
158
|
+
Originally Banned
|
|
159
|
+
</div>
|
|
160
|
+
<div style="font-size: 13px; color: #1A1F36; margin-top: 0.25rem;">
|
|
161
|
+
<%= @banned_ip.banned_at.strftime("%Y-%m-%d %H:%M:%S") %>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
<div>
|
|
165
|
+
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386;">
|
|
166
|
+
Time Banned
|
|
167
|
+
</div>
|
|
168
|
+
<div style="font-size: 13px; color: #1A1F36; margin-top: 0.25rem;">
|
|
169
|
+
<%= time_ago_in_words(@banned_ip.banned_at) %>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
<% if @banned_ip.updated_at != @banned_ip.created_at %>
|
|
173
|
+
<div>
|
|
174
|
+
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386;">
|
|
175
|
+
Last Modified
|
|
176
|
+
</div>
|
|
177
|
+
<div style="font-size: 13px; color: #1A1F36; margin-top: 0.25rem;">
|
|
178
|
+
<%= @banned_ip.updated_at.strftime("%Y-%m-%d %H:%M:%S") %>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
<% end %>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<!-- Submit Buttons -->
|
|
187
|
+
<div style="display: flex; gap: 0.5rem;">
|
|
188
|
+
<%= f.submit "Update Ban", class: "btn btn-primary" %>
|
|
189
|
+
<%= link_to "Cancel", banned_ip_path(@banned_ip), class: "btn btn-secondary" %>
|
|
190
|
+
<%= link_to "Delete Ban", banned_ip_path(@banned_ip),
|
|
191
|
+
data: {
|
|
192
|
+
"turbo-method": "delete",
|
|
193
|
+
"turbo-confirm": "Are you sure you want to unban #{@banned_ip.ip_address}?"
|
|
194
|
+
},
|
|
195
|
+
class: "btn btn-danger" %>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
<% end %>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<script>
|
|
203
|
+
function toggleDurationFields() {
|
|
204
|
+
const isPermanent = document.querySelector('input[name="banned_ip[permanent]"]:checked').value === 'true';
|
|
205
|
+
const temporaryOptions = document.getElementById('temporary-options');
|
|
206
|
+
|
|
207
|
+
if (isPermanent) {
|
|
208
|
+
temporaryOptions.style.display = 'none';
|
|
209
|
+
document.getElementById('banned_ip_expires_at').value = '';
|
|
210
|
+
} else {
|
|
211
|
+
temporaryOptions.style.display = 'block';
|
|
212
|
+
// If switching from permanent to temporary, set a default expiry
|
|
213
|
+
if (!document.getElementById('banned_ip_expires_at').value) {
|
|
214
|
+
const defaultExpiry = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours from now
|
|
215
|
+
document.getElementById('banned_ip_expires_at').value = defaultExpiry.toISOString().slice(0, 16);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function extendBan(hours) {
|
|
221
|
+
const expiresField = document.getElementById('banned_ip_expires_at');
|
|
222
|
+
let currentExpiry;
|
|
223
|
+
|
|
224
|
+
if (expiresField.value) {
|
|
225
|
+
currentExpiry = new Date(expiresField.value);
|
|
226
|
+
} else {
|
|
227
|
+
// Use current ban expiry or current time
|
|
228
|
+
<% if @banned_ip.expires_at %>
|
|
229
|
+
currentExpiry = new Date('<%= @banned_ip.expires_at.iso8601 %>');
|
|
230
|
+
<% else %>
|
|
231
|
+
currentExpiry = new Date();
|
|
232
|
+
<% end %>
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Add the specified hours
|
|
236
|
+
currentExpiry.setHours(currentExpiry.getHours() + hours);
|
|
237
|
+
|
|
238
|
+
// Update the field
|
|
239
|
+
expiresField.value = currentExpiry.toISOString().slice(0, 16);
|
|
240
|
+
|
|
241
|
+
// Show a visual feedback
|
|
242
|
+
const originalBg = expiresField.style.background;
|
|
243
|
+
expiresField.style.background = '#E8F5E9';
|
|
244
|
+
setTimeout(() => {
|
|
245
|
+
expiresField.style.background = originalBg;
|
|
246
|
+
}, 500);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function makePermanent() {
|
|
250
|
+
document.querySelector('input[name="banned_ip[permanent]"][value="true"]').checked = true;
|
|
251
|
+
toggleDurationFields();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Initialize on page load
|
|
255
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
256
|
+
// Set initial state
|
|
257
|
+
toggleDurationFields();
|
|
258
|
+
});
|
|
259
|
+
</script>
|