Add DeviceDetector and postres_cursor
This commit is contained in:
87
app/views/network_ranges/_geolite_data.html.erb
Normal file
87
app/views/network_ranges/_geolite_data.html.erb
Normal file
@@ -0,0 +1,87 @@
|
||||
<% geolite_data = network_range.network_data_for(:geolite) %>
|
||||
|
||||
<% if geolite_data.present? %>
|
||||
<div class="bg-white shadow rounded-lg mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">MaxMind GeoLite2 Data</h3>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<!-- ASN Data -->
|
||||
<% if geolite_data['asn'].present? %>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">ASN (MaxMind)</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
AS<%= geolite_data['asn']['autonomous_system_number'] %>
|
||||
<% if geolite_data['asn']['autonomous_system_organization'].present? %>
|
||||
<div class="text-xs text-gray-600"><%= geolite_data['asn']['autonomous_system_organization'] %></div>
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Country Data -->
|
||||
<% if geolite_data['country'].present? %>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Country (MaxMind)</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<%= geolite_data['country']['country_name'] || geolite_data['country']['country_iso_code'] %>
|
||||
<% if geolite_data['country']['country_iso_code'].present? %>
|
||||
<span class="ml-2 text-lg"><%= country_flag(geolite_data['country']['country_iso_code']) %></span>
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<% if geolite_data['country']['continent_name'].present? %>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Continent</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<%= geolite_data['country']['continent_name'] %>
|
||||
<span class="text-xs text-gray-500">(<%= geolite_data['country']['continent_code'] %>)</span>
|
||||
</dd>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if geolite_data['country']['geoname_id'].present? %>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">GeoName ID</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 font-mono">
|
||||
<%= geolite_data['country']['geoname_id'] %>
|
||||
</dd>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Flags -->
|
||||
<div class="md:col-span-2 lg:col-span-3">
|
||||
<dt class="text-sm font-medium text-gray-500 mb-2">MaxMind Flags</dt>
|
||||
<dd class="flex flex-wrap gap-2">
|
||||
<% if geolite_data['country']['is_anonymous_proxy'] %>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-100 text-red-800">Anonymous Proxy</span>
|
||||
<% end %>
|
||||
<% if geolite_data['country']['is_satellite_provider'] %>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-purple-100 text-purple-800">Satellite Provider</span>
|
||||
<% end %>
|
||||
<% if geolite_data['country']['is_anycast'] %>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">Anycast</span>
|
||||
<% end %>
|
||||
<% if geolite_data['country']['is_in_european_union'] == "1" %>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-600 text-white">🇪🇺 EU Member</span>
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Raw GeoLite Data (collapsible) -->
|
||||
<details class="mt-6 pt-6 border-t border-gray-200">
|
||||
<summary class="cursor-pointer text-sm font-medium text-gray-700 hover:text-gray-900">
|
||||
Show Raw MaxMind Data
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
<pre class="bg-gray-50 p-3 rounded-md text-xs overflow-x-auto"><%= JSON.pretty_generate(geolite_data) %></pre>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
112
app/views/network_ranges/_ipapi_data.html.erb
Normal file
112
app/views/network_ranges/_ipapi_data.html.erb
Normal file
@@ -0,0 +1,112 @@
|
||||
<div id="ipapi_data_section" class="bg-white shadow rounded-lg mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">IPAPI Enrichment Data</h3>
|
||||
</div>
|
||||
|
||||
<% if ipapi_loading %>
|
||||
<div class="px-6 py-8 text-center">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
<p class="mt-2 text-sm text-gray-500">Fetching enrichment data...</p>
|
||||
</div>
|
||||
<% elsif ipapi_data.present? %>
|
||||
<div class="px-6 py-4">
|
||||
<% if parent_with_ipapi %>
|
||||
<div class="mb-4 p-3 bg-blue-50 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>
|
||||
<span class="text-sm text-blue-800">
|
||||
Data inherited from parent network <%= link_to parent_with_ipapi.cidr, network_range_path(parent_with_ipapi), class: "font-mono font-medium hover:underline" %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<% if ipapi_data['asn'].present? %>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">ASN (IPAPI)</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
AS<%= ipapi_data['asn']['asn'] %>
|
||||
<% if ipapi_data['asn']['org'].present? %>
|
||||
<div class="text-xs text-gray-600"><%= ipapi_data['asn']['org'] %></div>
|
||||
<% end %>
|
||||
<% if ipapi_data['asn']['route'].present? %>
|
||||
<div class="text-xs text-gray-500 font-mono"><%= ipapi_data['asn']['route'] %></div>
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if ipapi_data['location'].present? %>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Location</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<%= [ipapi_data['location']['city'], ipapi_data['location']['state'], ipapi_data['location']['country']].compact.join(', ') %>
|
||||
<% if ipapi_data['location']['country_code'].present? %>
|
||||
<span class="ml-2 text-lg"><%= country_flag(ipapi_data['location']['country_code']) %></span>
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if ipapi_data['company'].present? %>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Company (IPAPI)</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<%= ipapi_data['company']['name'] %>
|
||||
<% if ipapi_data['company']['type'].present? %>
|
||||
<div class="text-xs text-gray-600"><%= ipapi_data['company']['type'].humanize %></div>
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if ipapi_data['is_datacenter'] || ipapi_data['is_vpn'] || ipapi_data['is_proxy'] || ipapi_data['is_tor'] %>
|
||||
<div class="md:col-span-2 lg:col-span-3">
|
||||
<dt class="text-sm font-medium text-gray-500 mb-2">IPAPI Flags</dt>
|
||||
<dd class="flex flex-wrap gap-2">
|
||||
<% if ipapi_data['is_datacenter'] %>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-orange-100 text-orange-800">Datacenter</span>
|
||||
<% end %>
|
||||
<% if ipapi_data['is_vpn'] %>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-purple-100 text-purple-800">VPN</span>
|
||||
<% end %>
|
||||
<% if ipapi_data['is_proxy'] %>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-100 text-red-800">Proxy</span>
|
||||
<% end %>
|
||||
<% if ipapi_data['is_tor'] %>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-800 text-white">Tor</span>
|
||||
<% end %>
|
||||
<% if ipapi_data['is_abuser'] %>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-600 text-white">Abuser</span>
|
||||
<% end %>
|
||||
<% if ipapi_data['is_bogon'] %>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-yellow-100 text-yellow-800">Bogon</span>
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Raw IPAPI Data (collapsible) -->
|
||||
<details class="mt-6 pt-6 border-t border-gray-200">
|
||||
<summary class="cursor-pointer text-sm font-medium text-gray-700 hover:text-gray-900">
|
||||
Show Raw IPAPI Data
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
<pre class="bg-gray-50 p-3 rounded-md text-xs overflow-x-auto"><%= JSON.pretty_generate(ipapi_data) %></pre>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<% 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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">No IPAPI data available</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Enrichment data will be fetched automatically.</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -1,5 +1,9 @@
|
||||
<% content_for :title, "#{@network_range.cidr} - Network Range Details" %>
|
||||
|
||||
<% if @network_range.persisted? %>
|
||||
<%= turbo_stream_from "network_range_#{@network_range.id}" %>
|
||||
<% end %>
|
||||
|
||||
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
@@ -48,6 +52,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IPAPI Enrichment Data -->
|
||||
<% if @network_range.persisted? %>
|
||||
<%= render partial: "network_ranges/ipapi_data", locals: {
|
||||
ipapi_data: @ipapi_data,
|
||||
network_range: @network_range,
|
||||
parent_with_ipapi: @parent_with_ipapi,
|
||||
ipapi_loading: @ipapi_loading || false
|
||||
} %>
|
||||
<% end %>
|
||||
|
||||
<!-- MaxMind GeoLite2 Data -->
|
||||
<% if @network_range.persisted? %>
|
||||
<%= render partial: "network_ranges/geolite_data", locals: {
|
||||
network_range: @network_range
|
||||
} %>
|
||||
<% end %>
|
||||
|
||||
<!-- Network Intelligence Card -->
|
||||
<div class="bg-white shadow rounded-lg mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
@@ -335,9 +356,12 @@
|
||||
<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>
|
||||
<%= form.text_field :expires_at,
|
||||
placeholder: "YYYY-MM-DD HH:MM (24-hour format, optional)",
|
||||
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
||||
data: { quick_create_rule_target: "expiresAtField" },
|
||||
autocomplete: "off" %>
|
||||
<p class="mt-1 text-xs text-gray-500">Leave blank for permanent rule. Format: YYYY-MM-DD HH:MM (e.g., 2024-12-31 23:59)</p>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-gray-600 flex items-center pt-6">
|
||||
@@ -461,9 +485,9 @@
|
||||
<div class="mt-1 text-sm text-gray-500">
|
||||
Created <%= time_ago_in_words(rule.created_at) %> ago by <%= rule.user&.email_address || 'System' %>
|
||||
</div>
|
||||
<% if rule.metadata&.dig('reason').present? %>
|
||||
<% if rule.metadata_hash['reason'].present? %>
|
||||
<div class="mt-1 text-sm text-gray-600">
|
||||
Reason: <%= rule.metadata['reason'] %>
|
||||
Reason: <%= rule.metadata_hash['reason'] %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user