Add DeviceDetector and postres_cursor

This commit is contained in:
Dan Milne
2025-11-13 08:35:00 +11:00
parent cc8213f87a
commit 2c7b801ed5
15 changed files with 472 additions and 158 deletions

View 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 %>

View 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>

View File

@@ -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>