Tidy up homepage and navigation
This commit is contained in:
343
app/views/analytics/index.html.erb
Normal file
343
app/views/analytics/index.html.erb
Normal file
@@ -0,0 +1,343 @@
|
||||
<% content_for :title, "Analytics Dashboard - Baffle Hub" %>
|
||||
|
||||
<div class="space-y-6" data-controller="dashboard" data-dashboard-period-value="<%= @time_period %>">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">Analytics Dashboard</h1>
|
||||
<p class="mt-2 text-gray-600">Overview of WAF events, rules, and network activity</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- Auto-refresh indicator -->
|
||||
<div class="flex items-center text-sm text-gray-500">
|
||||
<svg class="animate-spin h-4 w-4 mr-2 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Auto-refreshing
|
||||
</div>
|
||||
|
||||
<!-- Time Period Selector -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm text-gray-500">Time Period:</span>
|
||||
<div class="flex bg-white rounded-md shadow-sm border border-gray-300">
|
||||
<%= link_to "1H", analytics_path(period: :hour),
|
||||
class: time_period_class(:hour),
|
||||
data: { action: "click->dashboard#periodChanged", period: "hour" } %>
|
||||
<%= link_to "24H", analytics_path(period: :day),
|
||||
class: time_period_class(:day),
|
||||
data: { action: "click->dashboard#periodChanged", period: "day" } %>
|
||||
<%= link_to "1W", analytics_path(period: :week),
|
||||
class: time_period_class(:week),
|
||||
data: { action: "click->dashboard#periodChanged", period: "week" } %>
|
||||
<%= link_to "1M", analytics_path(period: :month),
|
||||
class: time_period_class(:month),
|
||||
data: { action: "click->dashboard#periodChanged", period: "month" } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Statistics Cards -->
|
||||
<div class="space-y-6">
|
||||
<div id="dashboard-stats" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<!-- Total Events -->
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M13 2.05v2.02c3.95.49 7 3.85 7 7.93 0 3.21-1.92 6-4.72 7.28l-.28.12V22l-5-5 5-5v2.4c2.21-.47 3.88-2.35 3.88-4.65 0-2.76-2.24-5-5-5-1.3 0-2.47.5-3.36 1.31L9 6.36C10.11 5.26 11.49 4.56 13 4.56V2.05c0-.45.54-.67.85-.35l8.78 8.78c.2.2.2.51 0 .71l-8.78 8.78c-.31.31-.85.1-.85-.35v-2.02c-5.05-.5-9-4.76-9-9.93 0-4.08 2.73-7.54 6.58-8.67L13 2.05z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Total Events</dt>
|
||||
<dd class="text-lg font-medium text-gray-900"><%= number_with_delimiter(@total_events) %></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-5 py-3">
|
||||
<div class="text-sm">
|
||||
<span class="text-green-600 font-medium">Last <%= @time_period.to_s.humanize %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Rules -->
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Active Rules</dt>
|
||||
<dd class="text-lg font-medium text-gray-900"><%= number_with_delimiter(@total_rules) %></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-5 py-3">
|
||||
<div class="text-sm">
|
||||
<span class="text-green-600 font-medium">Enabled</span>
|
||||
<% if @system_health[:disabled_rules] > 0 %>
|
||||
<span class="text-gray-500"> · <%= @system_health[:disabled_rules] %> disabled</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network Ranges with Events -->
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-purple-500 rounded-md flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Active Network Ranges</dt>
|
||||
<dd class="text-lg font-medium text-gray-900"><%= number_with_delimiter(@network_ranges_with_events) %></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-5 py-3">
|
||||
<div class="text-sm">
|
||||
<span class="text-purple-600 font-medium">of <%= number_with_delimiter(@total_network_ranges) %> total</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Health -->
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-orange-500 rounded-md flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">System Health</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">Normal</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-5 py-3">
|
||||
<div class="text-sm">
|
||||
<span class="text-green-600 font-medium">All systems operational</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts and Detailed Analytics -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Events Timeline Chart -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">Events Timeline (Last 24 Hours)</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="space-y-4">
|
||||
<% @chart_data[:timeline].each do |data| %>
|
||||
<div class="flex items-center">
|
||||
<div class="w-16 text-sm text-gray-500"><%= data[:time] %></div>
|
||||
<div class="flex-1 mx-4">
|
||||
<div class="bg-gray-200 rounded-full h-4">
|
||||
<div class="bg-blue-600 h-4 rounded-full"
|
||||
style="width: <%= [((data[:total].to_f / [@chart_data[:timeline].map { |d| d[:total] }.max, 1].max) * 100), 5].max %>%">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-12 text-sm text-gray-900 text-right"><%= data[:total] %></div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Actions Breakdown -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">Event Actions</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<% if @chart_data[:actions].any? %>
|
||||
<div class="space-y-4">
|
||||
<% @chart_data[:actions].each do |action| %>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="w-4 h-4 rounded mr-3
|
||||
<%= case action[:action]
|
||||
when 'Allow' then 'bg-green-500'
|
||||
when 'Deny', 'Block' then 'bg-red-500'
|
||||
when 'Challenge' then 'bg-yellow-500'
|
||||
else 'bg-gray-500'
|
||||
end %>">
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-900"><%= action[:action] %></span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span class="text-sm text-gray-600 mr-2"><%= number_with_delimiter(action[:count]) %></span>
|
||||
<span class="text-sm text-gray-500">(<%= action[:percentage] %>%)</span>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-gray-500 text-center py-8">No events in the selected time period</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Secondary Information Rows -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Top Countries -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">Top Countries</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<% if @top_countries.any? %>
|
||||
<div class="space-y-3">
|
||||
<% @top_countries.first(5).each do |country, count| %>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-900"><%= country %></span>
|
||||
<span class="text-sm font-medium text-gray-900"><%= number_with_delimiter(count) %></span>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-gray-500 text-center py-4">No country data available</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network Intelligence -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">Network Intelligence</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-900">🏢 Datacenter</span>
|
||||
<span class="text-sm font-medium text-gray-900"><%= number_with_delimiter(@network_intelligence[:datacenter_ranges]) %></span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-900">🔒 VPN</span>
|
||||
<span class="text-sm font-medium text-gray-900"><%= number_with_delimiter(@network_intelligence[:vpn_ranges]) %></span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-900">🛡️ Proxy</span>
|
||||
<span class="text-sm font-medium text-gray-900"><%= number_with_delimiter(@network_intelligence[:proxy_ranges]) %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div id="recent-activity" class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">Recent Activity</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="space-y-3">
|
||||
<% @recent_events.first(3).each do |event| %>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<div class="flex items-center">
|
||||
<div class="w-2 h-2 rounded-full mr-2
|
||||
<%= event.waf_action == 'allow' ? 'bg-green-500' : 'bg-red-500' %>"></div>
|
||||
<span class="text-gray-900 truncate max-w-[120px]"><%= event.ip_address %></span>
|
||||
</div>
|
||||
<span class="text-gray-500"><%= time_ago_in_words(event.timestamp) %> ago</span>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Blocked IPs -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium text-gray-900">Top Blocked IPs</h3>
|
||||
<%= link_to "View All Events", events_path, class: "text-sm text-blue-600 hover:text-blue-800" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<% if @top_blocked_ips.any? %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||
<% @top_blocked_ips.each do |ip, count| %>
|
||||
<div class="text-center p-3 bg-gray-50 rounded-lg">
|
||||
<div class="text-lg font-mono font-medium text-gray-900"><%= ip %></div>
|
||||
<div class="text-sm text-red-600"><%= number_with_delimiter(count) %> blocks</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-gray-500 text-center py-8">No blocked events in the selected time period</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">Quick Actions</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<%= link_to new_rule_path, class: "flex items-center justify-center px-4 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors" do %>
|
||||
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</svg>
|
||||
Create Rule
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_network_range_path, class: "flex items-center justify-center px-4 py-3 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors" do %>
|
||||
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</svg>
|
||||
Add Network Range
|
||||
<% end %>
|
||||
|
||||
<%= link_to events_path, class: "flex items-center justify-center px-4 py-3 bg-purple-600 text-white rounded-md hover:bg-purple-700 transition-colors" do %>
|
||||
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
|
||||
</svg>
|
||||
View Events
|
||||
<% end %>
|
||||
|
||||
<%= link_to rules_path, class: "flex items-center justify-center px-4 py-3 bg-orange-600 text-white rounded-md hover:bg-orange-700 transition-colors" do %>
|
||||
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z"/>
|
||||
</svg>
|
||||
Manage Rules
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
132
app/views/analytics/index.turbo_stream.erb
Normal file
132
app/views/analytics/index.turbo_stream.erb
Normal file
@@ -0,0 +1,132 @@
|
||||
<%= turbo_stream.replace "dashboard-stats" do %>
|
||||
<div id="dashboard-stats" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<!-- Total Events -->
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M13 2.05v2.02c3.95.49 7 3.85 7 7.93 0 3.21-1.92 6-4.72 7.28l-.28.12V22l-5-5 5-5v2.4c2.21-.47 3.88-2.35 3.88-4.65 0-2.76-2.24-5-5-5-1.3 0-2.47.5-3.36 1.31L9 6.36C10.11 5.26 11.49 4.56 13 4.56V2.05c0-.45.54-.67.85-.35l8.78 8.78c.2.2.2.51 0 .71l-8.78 8.78c-.31.31-.85.1-.85-.35v-2.02c-5.05-.5-9-4.76-9-9.93 0-4.08 2.73-7.54 6.58-8.67L13 2.05z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Total Events</dt>
|
||||
<dd class="text-lg font-medium text-gray-900"><%= number_with_delimiter(@total_events) %></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-5 py-3">
|
||||
<div class="text-sm">
|
||||
<span class="text-green-600 font-medium">Last <%= @time_period.to_s.humanize %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Rules -->
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Active Rules</dt>
|
||||
<dd class="text-lg font-medium text-gray-900"><%= number_with_delimiter(@total_rules) %></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-5 py-3">
|
||||
<div class="text-sm">
|
||||
<span class="text-green-600 font-medium">Enabled</span>
|
||||
<% if @system_health[:disabled_rules] > 0 %>
|
||||
<span class="text-gray-500"> · <%= @system_health[:disabled_rules] %> disabled</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network Ranges with Events -->
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-purple-500 rounded-md flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Active Network Ranges</dt>
|
||||
<dd class="text-lg font-medium text-gray-900"><%= number_with_delimiter(@network_ranges_with_events) %></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-5 py-3">
|
||||
<div class="text-sm">
|
||||
<span class="text-purple-600 font-medium">of <%= number_with_delimiter(@total_network_ranges) %> total</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Health -->
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-orange-500 rounded-md flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">System Health</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">Normal</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-5 py-3">
|
||||
<div class="text-sm">
|
||||
<span class="text-green-600 font-medium">All systems operational</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= turbo_stream.replace "recent-activity" do %>
|
||||
<div id="recent-activity" class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">Recent Activity</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="space-y-3">
|
||||
<% @recent_events.first(3).each do |event| %>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<div class="flex items-center">
|
||||
<div class="w-2 h-2 rounded-full mr-2
|
||||
<%= event.waf_action == 'allow' ? 'bg-green-500' : 'bg-red-500' %>"></div>
|
||||
<span class="text-gray-900 truncate max-w-[120px]"><%= event.ip_address %></span>
|
||||
</div>
|
||||
<span class="text-gray-500"><%= time_ago_in_words(event.timestamp) %> ago</span>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
95
app/views/dsns/edit.html.erb
Normal file
95
app/views/dsns/edit.html.erb
Normal file
@@ -0,0 +1,95 @@
|
||||
<% content_for :title, "Edit DSN - #{@dsn.name}" %>
|
||||
|
||||
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
||||
<div class="md:flex md:items-center md:justify-between mb-8">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h2 class="text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate">
|
||||
Edit DSN
|
||||
</h2>
|
||||
</div>
|
||||
<div class="mt-4 flex md:mt-0 md:ml-4">
|
||||
<%= link_to "Back to DSN", @dsn, class: "inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<%= form_with(model: @dsn, local: true, class: "space-y-6") do |form| %>
|
||||
<% if @dsn.errors.any? %>
|
||||
<div class="rounded-md bg-red-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">
|
||||
There were <%= pluralize(@dsn.errors.count, "error") %> with your submission:
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
<ul role="list" class="list-disc space-y-1 pl-5">
|
||||
<% @dsn.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
|
||||
<div class="sm:col-span-6">
|
||||
<%= form.label :name, class: "block text-sm font-medium text-gray-700" %>
|
||||
<div class="mt-1">
|
||||
<%= form.text_field :name, class: "shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md", placeholder: "e.g., Production DSN, Development DSN" %>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500">
|
||||
A descriptive name to help you identify this DSN key.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="sm:col-span-6">
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<%= form.check_box :enabled, class: "focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded" %>
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<%= form.label :enabled, class: "font-medium text-gray-700" %>
|
||||
<p class="text-gray-500">Enable this DSN for agent authentication</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DSN Key Display (Read-only) -->
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-md p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<h3 class="text-sm font-medium text-gray-800">
|
||||
DSN Key
|
||||
</h3>
|
||||
<div class="mt-1 text-sm text-gray-600 font-mono bg-white px-3 py-2 rounded border">
|
||||
<%= @dsn.key %>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500">DSN keys cannot be changed after creation.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-5">
|
||||
<div class="flex justify-end">
|
||||
<%= link_to "Cancel", @dsn, class: "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
|
||||
<%= form.submit "Update DSN", class: "ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
162
app/views/dsns/index.html.erb
Normal file
162
app/views/dsns/index.html.erb
Normal file
@@ -0,0 +1,162 @@
|
||||
<% content_for :title, "DSNs" %>
|
||||
|
||||
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">DSN Management</h1>
|
||||
<p class="mt-2 text-gray-600">Manage DSN keys for agent authentication</p>
|
||||
</div>
|
||||
<% if policy(Dsn).create? %>
|
||||
<div class="flex space-x-3">
|
||||
<%= link_to "New DSN", new_dsn_path, class: "inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Environment DSNs -->
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-md mb-8">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">Environment DSNs</h3>
|
||||
<p class="mt-1 max-w-2xl text-sm text-gray-500">
|
||||
Default DSNs configured via environment variables for agent connectivity.
|
||||
</p>
|
||||
</div>
|
||||
<div class="border-t border-gray-200">
|
||||
<dl>
|
||||
<!-- BAFFLE_HOST DSN -->
|
||||
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-500">External DSN (BAFFLE_HOST)</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<code class="bg-gray-100 px-2 py-1 rounded text-sm font-mono">
|
||||
<%= @external_dsn %>
|
||||
</code>
|
||||
<button onclick="copyToClipboard('<%= @external_dsn %>')" class="text-blue-600 hover:text-blue-800 text-sm">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-1">Host: <%= ENV['BAFFLE_HOST'] || 'localhost:3000' %></p>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<% if @internal_dsn.present? %>
|
||||
<!-- BAFFLE_INTERNAL_HOST DSN -->
|
||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-500">Internal DSN (BAFFLE_INTERNAL_HOST)</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<code class="bg-gray-100 px-2 py-1 rounded text-sm font-mono">
|
||||
<%= @internal_dsn %>
|
||||
</code>
|
||||
<button onclick="copyToClipboard('<%= @internal_dsn %>')" class="text-blue-600 hover:text-blue-800 text-sm">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-1">Host: <%= ENV['BAFFLE_INTERNAL_HOST'] %></p>
|
||||
</dd>
|
||||
</div>
|
||||
<% end %>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Database DSNs -->
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-md">
|
||||
<% if @dsns.any? %>
|
||||
<ul role="list" class="divide-y divide-gray-200">
|
||||
<% @dsns.each do |dsn| %>
|
||||
<li>
|
||||
<div class="px-4 py-4 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<% if dsn.enabled? %>
|
||||
<div class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-green-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="w-8 h-8 bg-red-100 rounded-full flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-red-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="flex items-center">
|
||||
<p class="text-sm font-medium text-gray-900"><%= dsn.name %></p>
|
||||
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <%= dsn.enabled? ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' %>">
|
||||
<%= dsn.enabled? ? 'Enabled' : 'Disabled' %>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 font-mono">
|
||||
Key: <%= dsn.key[0..15] + "..." %>
|
||||
</p>
|
||||
<p class="text-xs text-gray-400">
|
||||
Created: <%= dsn.created_at.strftime('%Y-%m-%d %H:%M') %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<%= link_to "View", dsn, class: "text-blue-600 hover:text-blue-900 text-sm font-medium" %>
|
||||
<% if policy(dsn).edit? %>
|
||||
<%= link_to "Edit", edit_dsn_path(dsn), class: "text-indigo-600 hover:text-indigo-900 text-sm font-medium" %>
|
||||
<% end %>
|
||||
<% if policy(dsn).disable? && dsn.enabled? %>
|
||||
<%= link_to "Disable", disable_dsn_path(dsn), method: :post,
|
||||
data: { confirm: "Are you sure you want to disable this DSN?" },
|
||||
class: "text-red-600 hover:text-red-900 text-sm font-medium" %>
|
||||
<% elsif policy(dsn).enable? && !dsn.enabled? %>
|
||||
<%= link_to "Enable", enable_dsn_path(dsn), method: :post,
|
||||
class: "text-green-600 hover:text-green-900 text-sm font-medium" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<div class="text-center py-12">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">No DSNs</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Get started by creating a new DSN.</p>
|
||||
<% if policy(Dsn).create? %>
|
||||
<div class="mt-6">
|
||||
<%= link_to "New DSN", new_dsn_path, class: "inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
// Show a brief confirmation
|
||||
const button = event.target.closest('button');
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg> Copied!';
|
||||
button.classList.add('text-green-600');
|
||||
button.classList.remove('text-blue-600');
|
||||
|
||||
setTimeout(function() {
|
||||
button.innerHTML = originalText;
|
||||
button.classList.remove('text-green-600');
|
||||
button.classList.add('text-blue-600');
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
93
app/views/dsns/new.html.erb
Normal file
93
app/views/dsns/new.html.erb
Normal file
@@ -0,0 +1,93 @@
|
||||
<% content_for :title, "New DSN" %>
|
||||
|
||||
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
||||
<div class="md:flex md:items-center md:justify-between mb-8">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h2 class="text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate">
|
||||
New DSN
|
||||
</h2>
|
||||
</div>
|
||||
<div class="mt-4 flex md:mt-0 md:ml-4">
|
||||
<%= link_to "Back to DSNs", dsns_path, class: "inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<%= form_with(model: @dsn, local: true, class: "space-y-6") do |form| %>
|
||||
<% if @dsn.errors.any? %>
|
||||
<div class="rounded-md bg-red-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">
|
||||
There were <%= pluralize(@dsn.errors.count, "error") %> with your submission:
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
<ul role="list" class="list-disc space-y-1 pl-5">
|
||||
<% @dsn.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
|
||||
<div class="sm:col-span-6">
|
||||
<%= form.label :name, class: "block text-sm font-medium text-gray-700" %>
|
||||
<div class="mt-1">
|
||||
<%= form.text_field :name, class: "shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md", placeholder: "e.g., Production DSN, Development DSN" %>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500">
|
||||
A descriptive name to help you identify this DSN key.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="sm:col-span-6">
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<%= form.check_box :enabled, class: "focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded" %>
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<%= form.label :enabled, class: "font-medium text-gray-700" %>
|
||||
<p class="text-gray-500">Enable this DSN for agent authentication</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-md p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-blue-800">
|
||||
DSN Key Information
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-blue-700">
|
||||
<p>A unique DSN key will be automatically generated when you create this DSN. This key will be used by your baffle-agents to authenticate with the hub.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-5">
|
||||
<div class="flex justify-end">
|
||||
<%= link_to "Cancel", dsns_path, class: "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
|
||||
<%= form.submit "Create DSN", class: "ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
138
app/views/dsns/show.html.erb
Normal file
138
app/views/dsns/show.html.erb
Normal file
@@ -0,0 +1,138 @@
|
||||
<% content_for :title, "DSN - #{@dsn.name}" %>
|
||||
|
||||
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
||||
<div class="md:flex md:items-center md:justify-between mb-8">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center">
|
||||
<h2 class="text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate">
|
||||
<%= @dsn.name %>
|
||||
</h2>
|
||||
<span class="ml-3 inline-flex items-center px-3 py-0.5 rounded-full text-sm font-medium <%= @dsn.enabled? ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' %>">
|
||||
<%= @dsn.enabled? ? 'Enabled' : 'Disabled' %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex md:mt-0 md:ml-4 space-x-3">
|
||||
<%= link_to "Back to DSNs", dsns_path, class: "inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" %>
|
||||
<% if policy(@dsn).edit? %>
|
||||
<%= link_to "Edit", edit_dsn_path(@dsn), class: "inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">DSN Information</h3>
|
||||
<p class="mt-1 max-w-2xl text-sm text-gray-500">
|
||||
DSN key details and usage information.
|
||||
</p>
|
||||
</div>
|
||||
<div class="border-t border-gray-200">
|
||||
<dl>
|
||||
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-500">Name</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2"><%= @dsn.name %></dd>
|
||||
</div>
|
||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-500">DSN Key</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<code class="bg-gray-100 px-2 py-1 rounded text-sm font-mono"><%= @dsn.key %></code>
|
||||
<button onclick="copyToClipboard('<%= @dsn.key %>')" class="text-blue-600 hover:text-blue-800 text-sm">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-500">Status</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <%= @dsn.enabled? ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' %>">
|
||||
<%= @dsn.enabled? ? 'Enabled' : 'Disabled' %>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-500">Created</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
||||
<%= @dsn.created_at.strftime('%B %d, %Y at %I:%M %p') %>
|
||||
</dd>
|
||||
</div>
|
||||
<% if @dsn.updated_at != @dsn.created_at %>
|
||||
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-500">Last Updated</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
||||
<%= @dsn.updated_at.strftime('%B %d, %Y at %I:%M %p') %>
|
||||
</dd>
|
||||
</div>
|
||||
<% end %>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<% if policy(@dsn).disable? || policy(@dsn).enable? %>
|
||||
<div class="mt-6 bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">Actions</h3>
|
||||
<div class="mt-5 space-x-3">
|
||||
<% if @dsn.enabled? && policy(@dsn).disable? %>
|
||||
<%= link_to "Disable DSN", disable_dsn_path(@dsn), method: :post,
|
||||
data: { confirm: "Are you sure you want to disable this DSN? Agents will no longer be able to authenticate with this key." },
|
||||
class: "inline-flex items-center px-4 py-2 border border-red-300 rounded-md shadow-sm text-sm font-medium text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500" %>
|
||||
<% elsif !@dsn.enabled? && policy(@dsn).enable? %>
|
||||
<%= link_to "Enable DSN", enable_dsn_path(@dsn), method: :post,
|
||||
class: "inline-flex items-center px-4 py-2 border border-green-300 rounded-md shadow-sm text-sm font-medium text-green-700 bg-white hover:bg-green-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Usage Instructions -->
|
||||
<div class="mt-6 bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">Usage Instructions</h3>
|
||||
<p class="mt-1 max-w-2xl text-sm text-gray-500">
|
||||
How to use this DSN key with your baffle-agents.
|
||||
</p>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 px-4 py-5 sm:px-6">
|
||||
<div class="prose max-w-none">
|
||||
<h4>HTTP Header Authentication</h4>
|
||||
<p>Include the DSN key in the <code>Authorization</code> header:</p>
|
||||
<pre class="bg-gray-100 p-3 rounded text-sm"><code>Authorization: Bearer <%= @dsn.key %></code></pre>
|
||||
|
||||
<h4 class="mt-4">Query Parameter Authentication</h4>
|
||||
<p>Include the DSN key as a query parameter:</p>
|
||||
<pre class="bg-gray-100 p-3 rounded text-sm"><code>/api/events?baffle_key=<%= @dsn.key %></code></pre>
|
||||
|
||||
<h4 class="mt-4">X-Baffle-Auth Header</h4>
|
||||
<p>Use the custom Baffle authentication header:</p>
|
||||
<pre class="bg-gray-100 p-3 rounded text-sm"><code>X-Baffle-Auth: Baffle baffle_key=<%= @dsn.key %>, baffle_version=1</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
// Show a brief confirmation
|
||||
const button = event.target.closest('button');
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg> Copied!';
|
||||
button.classList.add('text-green-600');
|
||||
button.classList.remove('text-blue-600');
|
||||
|
||||
setTimeout(function() {
|
||||
button.innerHTML = originalText;
|
||||
button.classList.remove('text-green-600');
|
||||
button.classList.add('text-blue-600');
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -1,112 +1,157 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1><%= @project.name %> - Events</h1>
|
||||
<% content_for :title, "Events - Baffle Hub" %>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div>
|
||||
<%= link_to "← Back to Project", @project, class: "btn btn-secondary" %>
|
||||
<%= link_to "Analytics", analytics_project_path(@project), class: "btn btn-info" %>
|
||||
<h1 class="text-3xl font-bold text-gray-900">Events</h1>
|
||||
<p class="mt-2 text-gray-600">WAF event log and analysis</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5>Filters</h5>
|
||||
<!-- Filters -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">Filters</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<%= form_with url: events_path, method: :get, local: true, class: "space-y-4" do |form| %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<%= form.label :ip, "IP Address", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_field :ip, value: params[:ip],
|
||||
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
||||
placeholder: "Filter by IP" %>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.label :waf_action, "Action", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.select :waf_action,
|
||||
options_for_select([['All', ''], ['Allow', 'allow'], ['Block', 'block'], ['Challenge', 'challenge']], params[:waf_action]),
|
||||
{ }, { class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" } %>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.label :country, "Country", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_field :country, value: params[:country],
|
||||
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
||||
placeholder: "Country code (e.g. US)" %>
|
||||
</div>
|
||||
<div class="flex items-end space-x-2">
|
||||
<%= form.submit "Apply Filters",
|
||||
class: "inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
|
||||
<%= link_to "Clear", events_path,
|
||||
class: "inline-flex justify-center py-2 px-4 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<%= form_with url: project_events_path(@project), method: :get, local: true, class: "row g-3" do |form| %>
|
||||
<div class="col-md-3">
|
||||
<%= form.label :ip, "IP Address", class: "form-label" %>
|
||||
<%= form.text_field :ip, value: params[:ip], class: "form-control", placeholder: "Filter by IP" %>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<%= form.label :waf_action, "Action", class: "form-label" %>
|
||||
<%= form.select :waf_action,
|
||||
options_for_select([['All', ''], ['Allow', 'allow'], ['Block', 'block'], ['Challenge', 'challenge']], params[:waf_action]),
|
||||
{}, { class: "form-select" } %>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<%= form.label :country, "Country", class: "form-label" %>
|
||||
<%= form.text_field :country, value: params[:country], class: "form-control", placeholder: "Country code (e.g. US)" %>
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<%= form.submit "Apply Filters", class: "btn btn-primary me-2" %>
|
||||
<%= link_to "Clear", project_events_path(@project), class: "btn btn-outline-secondary" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Events Table -->
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5>Events (<%= @events.count %>)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if @events.any? %>
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<!-- Events Table -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium text-gray-900">Events (<%= number_with_delimiter(@events.count) %>)</h3>
|
||||
<div class="flex items-center space-x-4">
|
||||
<%= link_to "📊 Analytics Dashboard", analytics_path,
|
||||
class: "text-sm text-blue-600 hover:text-blue-800 font-medium" %>
|
||||
<% if @pagy.pages > 1 %>
|
||||
<span class="text-sm text-gray-500">
|
||||
Page <%= @pagy.page %> of <%= @pagy.pages %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Top Pagination -->
|
||||
<% if @pagy.pages > 1 %>
|
||||
<div class="mt-4">
|
||||
<%= pagy_nav_tailwind(@pagy, pagy_id: 'events_top') %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<% if @events.any? %>
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>IP Address</th>
|
||||
<th>Action</th>
|
||||
<th>Path</th>
|
||||
<th>Method</th>
|
||||
<th>Status</th>
|
||||
<th>Country</th>
|
||||
<th>User Agent</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Time</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">IP Address</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Path</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Method</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Country</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">User Agent</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<% @events.each do |event| %>
|
||||
<tr>
|
||||
<td><%= event.timestamp.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
||||
<td><code><%= event.ip_address %></code></td>
|
||||
<td>
|
||||
<span class="badge bg-<%= event.blocked? ? 'danger' : event.allowed? ? 'success' : 'warning' %>">
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
<%= event.timestamp.strftime("%Y-%m-%d %H:%M:%S") %>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
|
||||
<%= event.ip_address %>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||
<%= case event.waf_action
|
||||
when 'allow' then 'bg-green-100 text-green-800'
|
||||
when 'deny', 'block' then 'bg-red-100 text-red-800'
|
||||
when 'challenge' then 'bg-yellow-100 text-yellow-800'
|
||||
else 'bg-gray-100 text-gray-800'
|
||||
end %>">
|
||||
<%= event.waf_action %>
|
||||
</span>
|
||||
</td>
|
||||
<td><code><%= event.request_path %></code></td>
|
||||
<td><%= event.request_method %></td>
|
||||
<td><%= event.response_status %></td>
|
||||
<td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900 max-w-xs truncate" title="<%= event.request_path %>">
|
||||
<%= event.request_path || '-' %>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
<%= event.request_method ? event.request_method.upcase : '-' %>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
<%= event.response_status || '-' %>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
<% if event.country_code.present? %>
|
||||
<span class="badge bg-light text-dark"><%= event.country_code %></span>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||
<%= event.country_code %>
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="text-muted">-</span>
|
||||
<span class="text-gray-400">-</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="text-truncate" style="max-width: 200px;" title="<%= event.user_agent %>">
|
||||
<%= event.user_agent&.truncate(30) || '-' %>
|
||||
<td class="px-6 py-4 text-sm text-gray-900 max-w-xs truncate" title="<%= event.user_agent %>">
|
||||
<%= event.user_agent&.truncate(50) || '-' %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<% if @pagy.pages > 1 %>
|
||||
<div class="d-flex justify-content-center mt-4">
|
||||
<%== pagy_nav(@pagy) %>
|
||||
</div>
|
||||
<div class="text-center text-muted mt-2">
|
||||
Showing <%= @pagy.from %> to <%= @pagy.to %> of <%= @pagy.count %> events
|
||||
<!-- Bottom Pagination -->
|
||||
<% if @pagy.pages > 1 %>
|
||||
<%= pagy_nav_tailwind(@pagy, pagy_id: 'events_bottom') %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="px-6 py-12 text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">No events</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
<% if params[:ip].present? || params[:waf_action].present? || params[:country].present? %>
|
||||
No events found matching your filters.
|
||||
<% else %>
|
||||
No events have been received yet.
|
||||
<% end %>
|
||||
</p>
|
||||
<% if params[:ip].present? || params[:waf_action].present? || params[:country].present? %>
|
||||
<div class="mt-6">
|
||||
<%= link_to "Clear Filters", events_path,
|
||||
class: "inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-blue-600 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="text-center py-5">
|
||||
<p class="text-muted mb-3">
|
||||
<% if params[:ip].present? || params[:waf_action].present? || params[:country].present? %>
|
||||
No events found matching your filters.
|
||||
<% else %>
|
||||
No events have been received yet.
|
||||
<% end %>
|
||||
</p>
|
||||
<% if params[:ip].present? || params[:waf_action].present? || params[:country].present? %>
|
||||
<%= link_to "Clear Filters", project_events_path(@project), class: "btn btn-outline-primary" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html class="bg-gray-50">
|
||||
<head>
|
||||
<title><%= content_for(:title) || "Baffle Hub - WAF Analytics" %></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
@@ -18,103 +18,196 @@
|
||||
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/icon.png">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
||||
<%= javascript_importmap_tags %>
|
||||
|
||||
<style>
|
||||
.badge { font-size: 0.8em; }
|
||||
/* Custom styles for code blocks and badges */
|
||||
code {
|
||||
background-color: #f8f9fa;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875em;
|
||||
@apply bg-gray-100 px-2 py-1 rounded text-sm font-mono;
|
||||
}
|
||||
.navbar-brand {
|
||||
font-weight: bold;
|
||||
|
||||
/* Flash message transitions */
|
||||
.flash-message {
|
||||
animation: slideInDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInDown {
|
||||
from {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container">
|
||||
<%= link_to "Baffle Hub", root_path, class: "navbar-brand" %>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<%= link_to "Projects", projects_path, class: "nav-link" %>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<%= link_to "Rules", rules_path, class: "nav-link" %>
|
||||
</li>
|
||||
<% if user_signed_in? && current_user_admin? %>
|
||||
<li class="nav-item">
|
||||
<%= link_to "Users", users_path, class: "nav-link" %>
|
||||
</li>
|
||||
<body class="min-h-screen bg-gray-50">
|
||||
<!-- Navigation Header -->
|
||||
<header class="bg-gray-900 shadow-sm border-b border-gray-800">
|
||||
<nav class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<!-- Logo and Main Navigation -->
|
||||
<div class="flex items-center">
|
||||
<!-- Logo -->
|
||||
<%= link_to root_path, class: "flex-shrink-0 flex items-center" do %>
|
||||
<svg class="h-8 w-8 text-blue-500 mr-3" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z"/>
|
||||
</svg>
|
||||
<span class="text-white text-lg font-bold">Baffle Hub</span>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav">
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:block ml-10">
|
||||
<div class="flex items-baseline space-x-4">
|
||||
<%= link_to "🔴 Events", events_path,
|
||||
class: nav_link_class(events_path) %>
|
||||
<%= link_to "⚙️ Rules", rules_path,
|
||||
class: nav_link_class(rules_path) %>
|
||||
<%= link_to "🌐 Network Ranges", network_ranges_path,
|
||||
class: nav_link_class(network_ranges_path) %>
|
||||
|
||||
<% if user_signed_in? && current_user_admin? %>
|
||||
<%= link_to "🔗 DSNs", dsns_path,
|
||||
class: nav_link_class(dsns_path) %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right side buttons -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<% if user_signed_in? %>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<!-- User dropdown -->
|
||||
<div class="relative" data-controller="dropdown">
|
||||
<button type="button"
|
||||
data-action="click->dropdown#toggle click@window->dropdown#hide"
|
||||
class="flex items-center text-white hover:bg-gray-800 px-3 py-2 rounded-md text-sm font-medium">
|
||||
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
|
||||
</svg>
|
||||
<%= current_user.email_address %>
|
||||
<span class="badge bg-secondary ms-1"><%= current_user.role %></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><%= link_to "Account Settings", edit_password_path, class: "dropdown-item" %></li>
|
||||
<% if current_user_admin? %>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><%= link_to "Manage Users", users_path, class: "dropdown-item" %></li>
|
||||
<% end %>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><%= link_to "Sign Out", session_path, data: { turbo_method: :delete }, class: "dropdown-item" %></li>
|
||||
</ul>
|
||||
</li>
|
||||
<span class="ml-2 px-2 py-0.5 text-xs rounded-full bg-blue-600 text-white">
|
||||
<%= current_user.role %>
|
||||
</span>
|
||||
<svg class="w-4 h-4 ml-1" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M7 10l5 5 5-5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<div data-dropdown-target="menu"
|
||||
class="hidden absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-50">
|
||||
<div class="py-1">
|
||||
<%= link_to edit_password_path,
|
||||
class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" do %>
|
||||
⚙️ Account Settings
|
||||
<% end %>
|
||||
|
||||
<% if current_user_admin? %>
|
||||
<div class="border-t border-gray-100"></div>
|
||||
<%= link_to users_path,
|
||||
class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" do %>
|
||||
👥 Manage Users
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<div class="border-t border-gray-100"></div>
|
||||
<%= link_to session_path,
|
||||
data: { turbo_method: :delete },
|
||||
class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" do %>
|
||||
🚪 Sign Out
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<% if User.none? %>
|
||||
<li class="nav-item">
|
||||
<%= link_to "Create Admin Account", new_registration_path, class: "nav-link btn btn-success text-white" %>
|
||||
</li>
|
||||
<%= link_to "👤 Create Admin Account", new_registration_path,
|
||||
class: "bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors" %>
|
||||
<% else %>
|
||||
<li class="nav-item">
|
||||
<%= link_to "Sign In", new_session_path, class: "nav-link" %>
|
||||
</li>
|
||||
<%= link_to "🔐 Sign In", new_session_path,
|
||||
class: "bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
<!-- Mobile menu button -->
|
||||
<div class="md:hidden">
|
||||
<button type="button"
|
||||
data-controller="mobile-menu"
|
||||
data-action="click->mobile-menu#toggle"
|
||||
class="text-white hover:bg-gray-800 p-2 rounded-md">
|
||||
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path data-mobile-menu-target="open" fill-rule="evenodd" d="M3 5h18v2H3V5zm0 6h18v2H3v-2zm0 6h18v2H3v-2z"/>
|
||||
<path data-mobile-menu-target="close" class="hidden" fill-rule="evenodd" d="M18.278 16.864a1 1 0 01-1.414 1.414l-4.829-4.828-4.828 4.828a1 1 0 01-1.414-1.414l4.828-4.829-4.828-4.828a1 1 0 011.414-1.414l4.829 4.828 4.828-4.828a1 1 0 111.414 1.414l-4.828 4.829 4.828 4.828z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu -->
|
||||
<div data-mobile-menu-target="menu" class="hidden md:hidden border-t border-gray-800">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1">
|
||||
<%= link_to "🔴 Events", events_path,
|
||||
class: mobile_nav_link_class(events_path) %>
|
||||
<%= link_to "⚙️ Rules", rules_path,
|
||||
class: mobile_nav_link_class(rules_path) %>
|
||||
<%= link_to "🌐 Network Ranges", network_ranges_path,
|
||||
class: mobile_nav_link_class(network_ranges_path) %>
|
||||
|
||||
<% if user_signed_in? && current_user_admin? %>
|
||||
<%= link_to "🔗 DSNs", dsns_path,
|
||||
class: mobile_nav_link_class(dsns_path) %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
<% if notice || alert %>
|
||||
<div class="fixed top-20 left-0 right-0 z-50 px-4 py-2">
|
||||
<% if notice %>
|
||||
<div class="flash-message bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded-md shadow-sm max-w-4xl mx-auto">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-green-600" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<%= notice %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if alert %>
|
||||
<div class="flash-message bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-md shadow-sm max-w-4xl mx-auto">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-red-600" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"/>
|
||||
</svg>
|
||||
<%= alert %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<%= yield %>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-gray-900 text-gray-400 mt-12">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="text-center">
|
||||
<p class="text-sm">© <%= Time.current.year %> Baffle Hub - WAF Analytics Platform</p>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
<% if notice %>
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<%= notice %>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if alert %>
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<%= alert %>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= yield %>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
91
app/views/layouts/authentication.html.erb
Normal file
91
app/views/layouts/authentication.html.erb
Normal file
@@ -0,0 +1,91 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="bg-gray-50">
|
||||
<head>
|
||||
<title><%= content_for(:title) || "Baffle Hub - WAF Analytics" %></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="application-name" content="Baffle Hub">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
|
||||
<%= yield :head %>
|
||||
|
||||
<link rel="icon" href="/icon.png" type="image/png">
|
||||
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/icon.png">
|
||||
|
||||
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
||||
<%= javascript_importmap_tags %>
|
||||
|
||||
<style>
|
||||
/* Custom styles for code blocks and badges */
|
||||
code {
|
||||
@apply bg-gray-100 px-2 py-1 rounded text-sm font-mono;
|
||||
}
|
||||
|
||||
/* Flash message transitions */
|
||||
.flash-message {
|
||||
animation: slideInDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInDown {
|
||||
from {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="min-h-screen bg-gray-50">
|
||||
<!-- Minimal Navigation for Auth Pages -->
|
||||
<%= render 'shared/auth_navigation' %>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
<% if notice || alert %>
|
||||
<div class="fixed top-20 left-0 right-0 z-50 px-4 py-2">
|
||||
<% if notice %>
|
||||
<div class="flash-message bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded-md shadow-sm max-w-4xl mx-auto">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-green-600" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<%= notice %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if alert %>
|
||||
<div class="flash-message bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-md shadow-sm max-w-4xl mx-auto">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-red-600" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"/>
|
||||
</svg>
|
||||
<%= alert %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="max-w-md mx-auto px-4 py-8">
|
||||
<%= yield %>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-white text-gray-500 mt-12 border-t border-gray-200">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="text-center">
|
||||
<p class="text-sm">© <%= Time.current.year %> Baffle Hub - WAF Analytics Platform</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,23 +1,21 @@
|
||||
<% content_for :title, "Network Ranges - #{@project.name}" %>
|
||||
<% content_for :title, "Network Ranges" %>
|
||||
|
||||
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">Network Ranges</h1>
|
||||
<p class="mt-2 text-gray-600">Browse and manage network ranges with intelligence data</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<%= link_to "IP Lookup", lookup_network_ranges_path, class: "inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" %>
|
||||
<%= link_to "Add Range", new_network_range_path, class: "inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700" %>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">Network Ranges</h1>
|
||||
<p class="mt-2 text-gray-600">Browse and manage network ranges with intelligence data</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<%= link_to "IP Lookup", lookup_network_ranges_path, class: "inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" %>
|
||||
<%= link_to "Add Range", new_network_range_path, class: "inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Filters -->
|
||||
<% if params[:asn].present? || params[:country].present? || params[:company].present? || params[:datacenter].present? || params[:vpn].present? || params[:proxy].present? || params[:source].present? || params[:search].present? %>
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-blue-900">Active Filters</h3>
|
||||
@@ -225,8 +223,23 @@
|
||||
<!-- Network Ranges Table -->
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-md">
|
||||
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">Network Ranges</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Showing <%= @network_ranges.count %> of <%= number_with_delimiter(@total_ranges) %> ranges</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900">Network Ranges</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Showing <%= @network_ranges.count %> of <%= number_with_delimiter(@total_ranges) %> ranges</p>
|
||||
</div>
|
||||
<% if @pagy.present? && @pagy.pages > 1 %>
|
||||
<span class="text-sm text-gray-500">
|
||||
Page <%= @pagy.page %> of <%= @pagy.pages %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<!-- Top Pagination -->
|
||||
<% if @pagy.present? && @pagy.pages > 1 %>
|
||||
<div class="mt-4">
|
||||
<%= pagy_nav_tailwind(@pagy, pagy_id: 'network_ranges_top') %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<ul class="divide-y divide-gray-200">
|
||||
@@ -304,10 +317,10 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<% if @pagy.present? %>
|
||||
<div class="mt-6 flex justify-center">
|
||||
<%= pagy_nav(@pagy) %>
|
||||
<!-- Bottom Pagination -->
|
||||
<% if @pagy.present? && @pagy.pages > 1 %>
|
||||
<div class="mt-6">
|
||||
<%= pagy_nav_tailwind(@pagy, pagy_id: 'network_ranges_bottom') %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -159,22 +159,22 @@
|
||||
<h3 class="text-lg font-medium text-gray-900">Traffic Statistics</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-gray-900"><%= number_with_delimiter(@traffic_stats[:total_requests]) %></div>
|
||||
<div class="text-sm text-gray-500">Total Requests</div>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="text-center min-w-0">
|
||||
<div class="text-2xl font-bold text-gray-900 break-words"><%= number_with_delimiter(@traffic_stats[:total_requests]) %></div>
|
||||
<div class="text-sm text-gray-500 whitespace-normal">Total Requests</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-gray-900"><%= number_with_delimiter(@traffic_stats[:unique_ips]) %></div>
|
||||
<div class="text-sm text-gray-500">Unique IPs</div>
|
||||
<div class="text-center min-w-0">
|
||||
<div class="text-2xl font-bold text-gray-900 break-words"><%= number_with_delimiter(@traffic_stats[:unique_ips]) %></div>
|
||||
<div class="text-sm text-gray-500 whitespace-normal">Unique IPs</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-green-600"><%= number_with_delimiter(@traffic_stats[:allowed_requests]) %></div>
|
||||
<div class="text-sm text-gray-500">Allowed</div>
|
||||
<div class="text-center min-w-0">
|
||||
<div class="text-2xl font-bold text-green-600 break-words"><%= number_with_delimiter(@traffic_stats[:allowed_requests]) %></div>
|
||||
<div class="text-sm text-gray-500 whitespace-normal">Allowed</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-red-600"><%= number_with_delimiter(@traffic_stats[:blocked_requests]) %></div>
|
||||
<div class="text-sm text-gray-500">Blocked</div>
|
||||
<div class="text-center min-w-0">
|
||||
<div class="text-2xl font-bold text-red-600 break-words"><%= number_with_delimiter(@traffic_stats[:blocked_requests]) %></div>
|
||||
<div class="text-sm text-gray-500 whitespace-normal">Blocked</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -196,11 +196,197 @@
|
||||
<% end %>
|
||||
|
||||
<!-- Associated Rules -->
|
||||
<% if @associated_rules.any? %>
|
||||
<div class="bg-white shadow rounded-lg mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<div class="bg-white shadow rounded-lg mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium text-gray-900">Associated Rules (<%= @associated_rules.count %>)</h3>
|
||||
<button type="button" onclick="toggleQuickCreateRule()" class="inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Quick Create Rule
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Create Rule Form -->
|
||||
<div id="quick_create_rule" class="hidden border-b border-gray-200">
|
||||
<div class="px-6 py-4 bg-blue-50">
|
||||
<%= form_with(model: Rule.new, url: rules_path, local: true,
|
||||
class: "space-y-4",
|
||||
data: { turbo: false }) do |form| %>
|
||||
|
||||
<!-- Hidden network range ID -->
|
||||
<%= form.hidden_field :network_range_id, value: @network_range.id %>
|
||||
|
||||
<!-- Network Context Display -->
|
||||
<div class="mb-4 p-3 bg-blue-100 border border-blue-200 rounded-md">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 text-blue-600 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<div class="text-sm text-blue-800">
|
||||
<strong>Creating rule for:</strong> <%= @network_range.cidr %>
|
||||
<% if @network_range.company.present? %>
|
||||
- <%= @network_range.company %>
|
||||
<% end %>
|
||||
<% if @network_range.asn_org.present? %>
|
||||
(ASN: <%= @network_range.asn_org %>)
|
||||
<% end %>
|
||||
<% if @network_range.is_datacenter? || @network_range.is_vpn? || @network_range.is_proxy? %>
|
||||
<span class="ml-2">
|
||||
<% if @network_range.is_datacenter? %><span class="bg-orange-200 px-2 py-0.5 rounded text-xs">Datacenter</span><% end %>
|
||||
<% if @network_range.is_vpn? %><span class="bg-purple-200 px-2 py-0.5 rounded text-xs">VPN</span><% end %>
|
||||
<% if @network_range.is_proxy? %><span class="bg-red-200 px-2 py-0.5 rounded text-xs">Proxy</span><% end %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Rule Type -->
|
||||
<div>
|
||||
<%= form.label :rule_type, "Rule Type", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.select :rule_type,
|
||||
options_for_select([
|
||||
['Network - IP/CIDR based blocking', 'network'],
|
||||
['Rate Limit - Request rate limiting', 'rate_limit'],
|
||||
['Path Pattern - URL path filtering', 'path_pattern'],
|
||||
['Header Pattern - HTTP header filtering', 'header_pattern'],
|
||||
['Query Pattern - Query parameter filtering', 'query_pattern'],
|
||||
['Body Signature - Request body filtering', 'body_signature']
|
||||
], 'network'),
|
||||
{ },
|
||||
{
|
||||
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
||||
id: "quick_rule_type_select",
|
||||
onchange: "toggleRuleTypeFields()"
|
||||
} %>
|
||||
<p class="mt-1 text-xs text-gray-500">Select the type of rule to create</p>
|
||||
</div>
|
||||
|
||||
<!-- Action -->
|
||||
<div>
|
||||
<%= form.label :action, "Action", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.select :action,
|
||||
options_for_select([
|
||||
['Deny - Block requests', 'deny'],
|
||||
['Allow - Whitelist requests', 'allow'],
|
||||
['Rate Limit - Throttle requests', 'rate_limit'],
|
||||
['Redirect - Redirect to URL', 'redirect'],
|
||||
['Challenge - Present CAPTCHA', 'challenge'],
|
||||
['Monitor - Log but allow', 'monitor']
|
||||
], 'deny'),
|
||||
{ },
|
||||
{ class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" } %>
|
||||
<p class="mt-1 text-xs text-gray-500">Action to take when rule matches</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expires At -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<%= form.label :expires_at, "Expires At (Optional)", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.datetime_local_field :expires_at,
|
||||
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
||||
<p class="mt-1 text-xs text-gray-500">Leave blank for permanent rule</p>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-gray-600 flex items-center pt-6">
|
||||
<svg class="w-4 h-4 text-blue-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Priority will be calculated automatically based on rule type
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pattern-based Rule Fields -->
|
||||
<div id="pattern_fields" class="hidden space-y-4">
|
||||
<div>
|
||||
<%= form.label :conditions, "Pattern/Conditions", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_area :conditions, rows: 3,
|
||||
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
||||
placeholder: "Enter pattern or JSON conditions...",
|
||||
id: "quick_conditions_field" %>
|
||||
<p class="mt-1 text-xs text-gray-500" id="pattern_help_text">Pattern will be used for matching</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rate Limit Fields -->
|
||||
<div id="rate_limit_fields" class="hidden space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<%= label_tag :rate_limit, "Request Limit", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= number_field_tag :rate_limit, 10,
|
||||
min: 1,
|
||||
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
||||
<p class="mt-1 text-xs text-gray-500">Maximum requests per time window</p>
|
||||
</div>
|
||||
<div>
|
||||
<%= label_tag :rate_window, "Time Window (seconds)", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= number_field_tag :rate_window, 60,
|
||||
min: 1,
|
||||
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
||||
<p class="mt-1 text-xs text-gray-500">Time window in seconds</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Redirect Fields -->
|
||||
<div id="redirect_fields" class="hidden space-y-4">
|
||||
<div>
|
||||
<%= label_tag :redirect_url, "Redirect URL", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= text_field_tag :redirect_url,
|
||||
"https://example.com/blocked",
|
||||
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
||||
<p class="mt-1 text-xs text-gray-500">URL to redirect to when rule matches</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metadata -->
|
||||
<div>
|
||||
<%= form.label :metadata, "Reason/Description", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_field :metadata,
|
||||
placeholder: "e.g., Block malicious traffic from this range",
|
||||
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
||||
<p class="mt-1 text-xs text-gray-500">Human-readable description of why this rule exists</p>
|
||||
</div>
|
||||
|
||||
<!-- Source -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<%= form.label :source, "Source", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.select :source,
|
||||
options_for_select([
|
||||
['Manual', 'manual'],
|
||||
['Auto-detected', 'auto'],
|
||||
['Hub Sync', 'hub'],
|
||||
['Imported', 'imported']
|
||||
], 'manual'),
|
||||
{ },
|
||||
{ class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" } %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center pt-6">
|
||||
<%= form.check_box :enabled, checked: true, class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %>
|
||||
<%= form.label :enabled, "Enable immediately", class: "ml-2 block text-sm text-gray-900" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="flex justify-end space-x-3 pt-4 border-t border-blue-200">
|
||||
<button type="button" onclick="toggleQuickCreateRule()" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
|
||||
Cancel
|
||||
</button>
|
||||
<%= form.submit "Create Rule", class: "px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rules List -->
|
||||
<% if @associated_rules.any? %>
|
||||
<div class="divide-y divide-gray-200">
|
||||
<% @associated_rules.each do |rule| %>
|
||||
<div class="px-6 py-4">
|
||||
@@ -214,6 +400,9 @@
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
Priority: <%= rule.priority %>
|
||||
</span>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||
<%= rule.rule_type.humanize %>
|
||||
</span>
|
||||
<% if rule.source.include?('surgical') %>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
|
||||
Surgical
|
||||
@@ -242,8 +431,16 @@
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="px-6 py-8 text-center text-gray-500">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">No rules yet</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Get started by creating a rule for this network range.</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Network Relationships -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
@@ -344,4 +541,87 @@
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleQuickCreateRule() {
|
||||
const formDiv = document.getElementById('quick_create_rule');
|
||||
formDiv.classList.toggle('hidden');
|
||||
|
||||
// Reset form when hiding
|
||||
if (formDiv.classList.contains('hidden')) {
|
||||
resetQuickCreateForm();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRuleTypeFields() {
|
||||
const ruleType = document.getElementById('quick_rule_type_select').value;
|
||||
const action = document.querySelector('select[name="rule[action]"]').value;
|
||||
|
||||
// Hide all optional fields
|
||||
document.getElementById('pattern_fields').classList.add('hidden');
|
||||
document.getElementById('rate_limit_fields').classList.add('hidden');
|
||||
document.getElementById('redirect_fields').classList.add('hidden');
|
||||
|
||||
// Show relevant fields based on rule type
|
||||
if (['path_pattern', 'header_pattern', 'query_pattern', 'body_signature'].includes(ruleType)) {
|
||||
document.getElementById('pattern_fields').classList.remove('hidden');
|
||||
updatePatternHelpText(ruleType);
|
||||
} else if (ruleType === 'rate_limit') {
|
||||
document.getElementById('rate_limit_fields').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Show redirect fields if action is redirect
|
||||
if (action === 'redirect') {
|
||||
document.getElementById('redirect_fields').classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function updatePatternHelpText(ruleType) {
|
||||
const helpText = document.getElementById('pattern_help_text');
|
||||
const conditionsField = document.getElementById('quick_conditions_field');
|
||||
|
||||
switch(ruleType) {
|
||||
case 'path_pattern':
|
||||
helpText.textContent = 'Regex pattern to match URL paths (e.g.,\\.env$|wp-admin|phpmyadmin)';
|
||||
conditionsField.placeholder = 'Example: \\.env$|\\.git|config\\.php|wp-admin';
|
||||
break;
|
||||
case 'header_pattern':
|
||||
helpText.textContent = 'JSON with header_name and pattern (e.g., {"header_name": "User-Agent", "pattern": "bot.*"})';
|
||||
conditionsField.placeholder = 'Example: {"header_name": "User-Agent", "pattern": ".*[Bb]ot.*"}';
|
||||
break;
|
||||
case 'query_pattern':
|
||||
helpText.textContent = 'Regex pattern to match query parameters (e.g., union.*select|<script)';
|
||||
conditionsField.placeholder = 'Example: (?:union|select|insert|update|delete).*\\s+(?:union|select)';
|
||||
break;
|
||||
case 'body_signature':
|
||||
helpText.textContent = 'Regex pattern to match request body content (e.g., OR 1=1|<script)';
|
||||
conditionsField.placeholder = 'Example: (?:OR\\s+1\\s*=\\s*1|AND\\s+1\\s*=\\s*1|UNION\\s+SELECT)';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function resetQuickCreateForm() {
|
||||
const form = document.querySelector('#quick_create_rule form');
|
||||
if (form) {
|
||||
form.reset();
|
||||
// Reset rule type to default
|
||||
document.getElementById('quick_rule_type_select').value = 'network';
|
||||
toggleRuleTypeFields();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the form visibility state
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Set up action change listener to show/hide redirect fields
|
||||
const actionSelect = document.querySelector('select[name="rule[action]"]');
|
||||
if (actionSelect) {
|
||||
actionSelect.addEventListener('change', function() {
|
||||
toggleRuleTypeFields();
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize field visibility
|
||||
toggleRuleTypeFields();
|
||||
});
|
||||
</script>
|
||||
@@ -1,200 +0,0 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1><%= @project.name %> - Analytics</h1>
|
||||
<div>
|
||||
<%= link_to "← Back to Project", project_path(@project), class: "btn btn-secondary" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Time Range Selector -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5>Time Range</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<%= form_with url: analytics_project_path(@project), method: :get, local: true do |form| %>
|
||||
<div class="row align-items-end">
|
||||
<div class="col-md-6">
|
||||
<%= form.label :time_range, "Time Range", class: "form-label" %>
|
||||
<%= form.select :time_range,
|
||||
options_for_select([
|
||||
["Last Hour", 1],
|
||||
["Last 6 Hours", 6],
|
||||
["Last 24 Hours", 24],
|
||||
["Last 7 Days", 168],
|
||||
["Last 30 Days", 720]
|
||||
], @time_range),
|
||||
{}, class: "form-select" %>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<%= form.submit "Update", class: "btn btn-primary" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary Stats -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h3 class="text-primary"><%= number_with_delimiter(@total_events) %></h3>
|
||||
<p class="card-text">Total Events</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h3 class="text-success"><%= number_with_delimiter(@allowed_events) %></h3>
|
||||
<p class="card-text">Allowed</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h3 class="text-danger"><%= number_with_delimiter(@blocked_events) %></h3>
|
||||
<p class="card-text">Blocked</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Top Blocked IPs -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Top Blocked IPs</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if @top_blocked_ips.any? %>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP Address</th>
|
||||
<th>Blocked Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @top_blocked_ips.each do |stat| %>
|
||||
<tr>
|
||||
<td><code><%= stat.ip_address %></code></td>
|
||||
<td><%= number_with_delimiter(stat.count) %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-muted">No blocked events in this time range.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Country Distribution -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Top Countries</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if @country_stats.any? %>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Country</th>
|
||||
<th>Events</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @country_stats.each do |stat| %>
|
||||
<tr>
|
||||
<td><%= stat.country_code || 'Unknown' %></td>
|
||||
<td><%= number_with_delimiter(stat.count) %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-muted">No country data available.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Distribution -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Action Distribution</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if @action_stats.any? %>
|
||||
<div class="row">
|
||||
<% @action_stats.each do |stat| %>
|
||||
<div class="col-md-3 text-center mb-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h4><%= stat.action.upcase %></h4>
|
||||
<p class="card-text">
|
||||
<span class="badge bg-<%=
|
||||
case stat.action
|
||||
when 'allow', 'pass' then 'success'
|
||||
when 'block', 'deny' then 'danger'
|
||||
when 'challenge' then 'warning'
|
||||
when 'rate_limit' then 'info'
|
||||
else 'secondary'
|
||||
end %>">
|
||||
<%= number_with_delimiter(stat.count) %>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-muted">No action data available.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if @total_events > 0 %>
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Block Rate</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="progress" style="height: 30px;">
|
||||
<% blocked_percentage = (@blocked_events.to_f / @total_events * 100).round(1) %>
|
||||
<% allowed_percentage = (@allowed_events.to_f / @total_events * 100).round(1) %>
|
||||
|
||||
<div class="progress-bar bg-success" style="width: <%= allowed_percentage %>%">
|
||||
<%= allowed_percentage %>% Allowed
|
||||
</div>
|
||||
<div class="progress-bar bg-danger" style="width: <%= blocked_percentage %>%">
|
||||
<%= blocked_percentage %>% Blocked
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="mt-4">
|
||||
<%= link_to "View Events", events_project_path(@project), class: "btn btn-primary" %>
|
||||
<%= link_to "Export Data", "#", class: "btn btn-secondary", onclick: "alert('Export feature coming soon!')" %>
|
||||
</div>
|
||||
@@ -1,49 +0,0 @@
|
||||
<h1>Projects</h1>
|
||||
|
||||
<%= link_to "New Project", new_project_path, class: "btn btn-primary mb-3" %>
|
||||
|
||||
<div class="row">
|
||||
<% @projects.each do |project| %>
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><%= project.name %></h5>
|
||||
<span class="badge <%= project.enabled? ? 'bg-success' : 'bg-secondary' %>">
|
||||
<%= project.enabled? ? 'Active' : 'Disabled' %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">
|
||||
<strong>Status:</strong>
|
||||
<span class="badge bg-<%= project.waf_status == 'active' ? 'success' : project.waf_status == 'idle' ? 'warning' : 'danger' %>">
|
||||
<%= project.waf_status %>
|
||||
</span>
|
||||
</p>
|
||||
<p class="card-text">
|
||||
<strong>Events (24h):</strong> <%= project.event_count(24.hours.ago) %>
|
||||
</p>
|
||||
<p class="card-text">
|
||||
<strong>Blocked (24h):</strong> <%= project.blocked_count(24.hours.ago) %>
|
||||
</p>
|
||||
<small class="text-muted">
|
||||
<strong>DSN:</strong><br>
|
||||
<code><%= project.dsn %></code>
|
||||
</small>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<%= link_to "View", project_path(project), class: "btn btn-primary btn-sm" %>
|
||||
<%= link_to "Events", project_events_path(project), class: "btn btn-secondary btn-sm" %>
|
||||
<%= link_to "Analytics", analytics_project_path(project), class: "btn btn-info btn-sm" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if @projects.empty? %>
|
||||
<div class="text-center my-5">
|
||||
<h3>No projects yet</h3>
|
||||
<p>Create your first project to start monitoring WAF events.</p>
|
||||
<%= link_to "Create Project", new_project_path, class: "btn btn-primary" %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -1,32 +0,0 @@
|
||||
<h1>New Project</h1>
|
||||
|
||||
<%= form_with(model: @project, local: true) do |form| %>
|
||||
<% if @project.errors.any? %>
|
||||
<div class="alert alert-danger">
|
||||
<h4><%= pluralize(@project.errors.count, "error") %> prohibited this project from being saved:</h4>
|
||||
<ul>
|
||||
<% @project.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= form.label :name, class: "form-label" %>
|
||||
<%= form.text_field :name, class: "form-control" %>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= form.label :enabled, class: "form-label" %>
|
||||
<div class="form-check">
|
||||
<%= form.check_box :enabled, class: "form-check-input" %>
|
||||
<%= form.label :enabled, "Enable this project", class: "form-check-label" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= form.submit "Create Project", class: "btn btn-primary" %>
|
||||
<%= link_to "Cancel", projects_path, class: "btn btn-secondary" %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -1,118 +0,0 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1><%= @project.name %></h1>
|
||||
<div>
|
||||
<%= link_to "Edit", edit_project_path(@project), class: "btn btn-secondary" %>
|
||||
<%= link_to "Events", project_events_path(@project), class: "btn btn-primary" %>
|
||||
<%= link_to "Analytics", analytics_project_path(@project), class: "btn btn-info" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Project Status</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>Status:</strong>
|
||||
<span class="badge bg-<%= @waf_status == 'active' ? 'success' : @waf_status == 'idle' ? 'warning' : 'danger' %>">
|
||||
<%= @waf_status %>
|
||||
</span>
|
||||
</p>
|
||||
<p><strong>Enabled:</strong>
|
||||
<span class="badge bg-<%= @project.enabled? ? 'success' : 'secondary' %>">
|
||||
<%= @project.enabled? ? 'Yes' : 'No' %>
|
||||
</span>
|
||||
</p>
|
||||
<p><strong>Events (24h):</strong> <%= @event_count %></p>
|
||||
<p><strong>Blocked (24h):</strong> <%= @blocked_count %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>DSN Configuration</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>DSN:</strong></p>
|
||||
<code><%= @project.dsn %></code>
|
||||
<button class="btn btn-sm btn-outline-primary ms-2" onclick="copyDSN()">Copy</button>
|
||||
|
||||
<% if @project.internal_dsn.present? %>
|
||||
<hr>
|
||||
<p><strong>Internal DSN:</strong></p>
|
||||
<code><%= @project.internal_dsn %></code>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Recent Events</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if @recent_events.any? %>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>IP</th>
|
||||
<th>Action</th>
|
||||
<th>Path</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @recent_events.limit(5).each do |event| %>
|
||||
<tr>
|
||||
<td><%= event.timestamp.strftime("%H:%M:%S") %></td>
|
||||
<td><%= event.ip_address %></td>
|
||||
<td>
|
||||
<span class="badge bg-<%= event.blocked? ? 'danger' : event.allowed? ? 'success' : 'warning' %>">
|
||||
<%= event.waf_action %>
|
||||
</span>
|
||||
</td>
|
||||
<td><code><%= event.request_path %></code></td>
|
||||
<td><%= event.response_status %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<%= link_to "View All Events", project_events_path(@project), class: "btn btn-primary btn-sm" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-muted">No events received yet.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyDSN() {
|
||||
const dsnElement = document.querySelector('code');
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = dsnElement.textContent;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
|
||||
// Show feedback
|
||||
const button = event.target;
|
||||
const originalText = button.textContent;
|
||||
button.textContent = 'Copied!';
|
||||
button.classList.add('btn-success');
|
||||
setTimeout(() => {
|
||||
button.textContent = originalText;
|
||||
button.classList.remove('btn-success');
|
||||
}, 2000);
|
||||
}
|
||||
</script>
|
||||
@@ -1,22 +1,19 @@
|
||||
<% content_for :title, "Rules - #{@project.name}" %>
|
||||
<% content_for :title, "Rules" %>
|
||||
|
||||
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">Rules</h1>
|
||||
<p class="mt-2 text-gray-600">Manage WAF rules for traffic filtering and control</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<%= link_to "Add Network Range", new_network_range_path, class: "inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" %>
|
||||
<%= link_to "Create Rule", new_rule_path, class: "inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700" %>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">Rules</h1>
|
||||
<p class="mt-2 text-gray-600">Manage WAF rules for traffic filtering and control</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<%= link_to "Create Rule", new_rule_path, class: "inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
@@ -93,7 +90,20 @@
|
||||
<!-- Rules List -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">All Rules</h3>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium text-gray-900">All Rules (<%= number_with_delimiter(@rules.count) %>)</h3>
|
||||
<% if @pagy.present? && @pagy.pages > 1 %>
|
||||
<span class="text-sm text-gray-500">
|
||||
Page <%= @pagy.page %> of <%= @pagy.pages %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<!-- Top Pagination -->
|
||||
<% if @pagy.present? && @pagy.pages > 1 %>
|
||||
<div class="mt-4">
|
||||
<%= pagy_nav_tailwind(@pagy, pagy_id: 'rules_top') %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if @rules.any? %>
|
||||
@@ -178,7 +188,7 @@
|
||||
|
||||
<% if rule.expires_at.present? %>
|
||||
<span class="text-xs text-gray-500" title="Expires at <%= rule.expires_at.strftime('%Y-%m-%d %H:%M') %>">
|
||||
<%= distance_of_time_in_words(Time.current, rule.expires_at) %> left
|
||||
Expires <%= time_ago_in_words(rule.expires_at) %> from now
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -221,4 +231,11 @@
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Pagination -->
|
||||
<% if @pagy.present? && @pagy.pages > 1 %>
|
||||
<div class="mt-6">
|
||||
<%= pagy_nav_tailwind(@pagy, pagy_id: 'rules_bottom') %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
24
app/views/shared/_auth_navigation.html.erb
Normal file
24
app/views/shared/_auth_navigation.html.erb
Normal file
@@ -0,0 +1,24 @@
|
||||
<!-- Minimal Navigation Header for Authentication Pages -->
|
||||
<header class="bg-white border-b border-gray-200">
|
||||
<nav class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<!-- Logo -->
|
||||
<div class="flex items-center">
|
||||
<%= link_to root_path, class: "flex-shrink-0 flex items-center" do %>
|
||||
<svg class="h-8 w-8 text-blue-500 mr-3" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z"/>
|
||||
</svg>
|
||||
<span class="text-gray-900 text-lg font-bold">Baffle Hub</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Right side - optional help link -->
|
||||
<div class="flex items-center">
|
||||
<% if User.any? %>
|
||||
<%= link_to "🏠 Back to Dashboard", root_path,
|
||||
class: "text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium transition-colors" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
Reference in New Issue
Block a user