Add new IP address search page.
Add a new IP address search page at /ip_addresses. Replaces the old search page at /moderator/ip_addrs. On user profile pages, show the user's last known IP to mods. Also add search links for finding other IPs or accounts associated with the user. IP address search uses a big UNION ALL statement to merge IP addresses across various tables into a single view. This makes searching easier, but is known to timeout in certain cases. Fixes #4207 (the new IP search page supports searching by subnet).
This commit is contained in:
17
app/controllers/ip_addresses_controller.rb
Normal file
17
app/controllers/ip_addresses_controller.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
class IpAddressesController < ApplicationController
|
||||
respond_to :html, :xml, :json
|
||||
before_action :moderator_only
|
||||
|
||||
def index
|
||||
if search_params[:group_by] == "ip_addr"
|
||||
@ip_addresses = IpAddress.search(search_params).group_by_ip_addr
|
||||
respond_with(@ip_addresses)
|
||||
elsif search_params[:group_by] == "user"
|
||||
@ip_addresses = IpAddress.search(search_params).group_by_user
|
||||
respond_with(@ip_addresses)
|
||||
else
|
||||
@ip_addresses = IpAddress.includes(:user, :model).paginated_search(params)
|
||||
respond_with(@ip_addresses)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -109,7 +109,7 @@ module ApplicationHelper
|
||||
end
|
||||
|
||||
def link_to_ip(ip)
|
||||
link_to ip, moderator_ip_addrs_path(:search => {:ip_addr => ip})
|
||||
link_to ip, ip_addresses_path(search: { ip_addr: ip, group_by: "user" })
|
||||
end
|
||||
|
||||
def link_to_search(search)
|
||||
|
||||
9
app/logical/ip_address_type.rb
Normal file
9
app/logical/ip_address_type.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
class IpAddressType < ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Inet
|
||||
def cast(value)
|
||||
super(IPAddress.parse(value))
|
||||
end
|
||||
|
||||
def serialize(value)
|
||||
value.to_string
|
||||
end
|
||||
end
|
||||
@@ -50,6 +50,11 @@ class ApplicationRecord < ActiveRecord::Base
|
||||
where.not("#{qualified_column_for(attr)} ~ ?", "(?e)" + value)
|
||||
end
|
||||
|
||||
def where_inet_matches(attr, value)
|
||||
ip = IPAddress.parse(value)
|
||||
where("#{qualified_column_for(attr)} <<= ?", ip.to_string)
|
||||
end
|
||||
|
||||
def where_array_includes_any(attr, values)
|
||||
where("#{qualified_column_for(attr)} && ARRAY[?]", values)
|
||||
end
|
||||
@@ -79,6 +84,14 @@ class ApplicationRecord < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
def search_inet_attribute(attr, params)
|
||||
if params[attr].present?
|
||||
where_inet_matches(attr, params[attr])
|
||||
else
|
||||
all
|
||||
end
|
||||
end
|
||||
|
||||
# range: "5", ">5", "<5", ">=5", "<=5", "5..10", "5,6,7"
|
||||
def numeric_attribute_matches(attribute, range)
|
||||
return all unless range.present?
|
||||
@@ -130,6 +143,8 @@ class ApplicationRecord < ActiveRecord::Base
|
||||
search_boolean_attribute(name, params)
|
||||
when :integer, :datetime
|
||||
numeric_attribute_matches(name, params[name])
|
||||
when :inet
|
||||
search_inet_attribute(name, params)
|
||||
else
|
||||
raise NotImplementedError, "unhandled attribute type"
|
||||
end
|
||||
|
||||
28
app/models/ip_address.rb
Normal file
28
app/models/ip_address.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
class IpAddress < ApplicationRecord
|
||||
belongs_to :model, polymorphic: true
|
||||
belongs_to :user
|
||||
attribute :ip_addr, IpAddressType.new
|
||||
|
||||
def self.model_types
|
||||
%w[Post User Comment Dmail ArtistVersion ArtistCommentaryVersion NoteVersion WikiPageVersion]
|
||||
end
|
||||
|
||||
def self.search(params)
|
||||
q = super
|
||||
q = q.where.not(model_type: "Dmail") unless CurrentUser.is_admin?
|
||||
q = q.search_attributes(params, :user, :model_type, :model_id, :ip_addr)
|
||||
q.order(created_at: :desc)
|
||||
end
|
||||
|
||||
def self.group_by_ip_addr
|
||||
group(:ip_addr).select("ip_addr, COUNT(*) AS count_all").reorder("count_all DESC, ip_addr")
|
||||
end
|
||||
|
||||
def self.group_by_user
|
||||
group(:user_id).select("user_id, COUNT(*) AS count_all").reorder("count_all DESC, user_id")
|
||||
end
|
||||
|
||||
def readonly?
|
||||
true
|
||||
end
|
||||
end
|
||||
30
app/views/ip_addresses/_index.html.erb
Normal file
30
app/views/ip_addresses/_index.html.erb
Normal file
@@ -0,0 +1,30 @@
|
||||
<table class="striped autofit">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Source</th>
|
||||
<th>ID</th>
|
||||
<th>IP Address</th>
|
||||
<th>User</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @ip_addresses.each do |ip| %>
|
||||
<tr>
|
||||
<td><%= link_to ip.model_type.underscore.humanize, ip_addresses_path(search: { model_type: ip.model_type }) %></td>
|
||||
<td><%= link_to "##{ip.model_id}", ip.model %></td>
|
||||
<td>
|
||||
<%= link_to ip.ip_addr, ip_addresses_path(search: { ip_addr: ip.ip_addr }) %>
|
||||
<%= link_to "»", ip_addresses_path(search: { ip_addr: ip.ip_addr, group_by: "user" }) %>
|
||||
</td>
|
||||
<td>
|
||||
<%= link_to_user ip.user %>
|
||||
<%= link_to "»", ip_addresses_path(search: { user_id: ip.user_id, group_by: "ip_addr" }) %>
|
||||
</td>
|
||||
<td><%= time_ago_in_words_tagged ip.created_at %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<%= numbered_paginator(@ip_addresses) %>
|
||||
19
app/views/ip_addresses/_index_by_ip_addr.html.erb
Normal file
19
app/views/ip_addresses/_index_by_ip_addr.html.erb
Normal file
@@ -0,0 +1,19 @@
|
||||
<table class="striped autofit">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP Address</th>
|
||||
<th>Uses</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @ip_addresses.each do |ip| %>
|
||||
<tr>
|
||||
<td>
|
||||
<%= link_to ip.ip_addr, ip_addresses_path(search: { ip_addr: ip.ip_addr }) %>
|
||||
<%= link_to "»", ip_addresses_path(search: { ip_addr: ip.ip_addr, group_by: "user" }) %>
|
||||
</td>
|
||||
<td><%= link_to ip.count_all, ip_addresses_path(search: { ip_addr: ip.ip_addr }) %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
19
app/views/ip_addresses/_index_by_user.html.erb
Normal file
19
app/views/ip_addresses/_index_by_user.html.erb
Normal file
@@ -0,0 +1,19 @@
|
||||
<table class="striped autofit">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Uses</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @ip_addresses.each do |ip| %>
|
||||
<tr>
|
||||
<td>
|
||||
<%= link_to_user ip.user %>
|
||||
<%= link_to "»", ip_addresses_path(search: { user_id: ip.user_id, group_by: "ip_addr" }) %>
|
||||
</td>
|
||||
<td><%= link_to ip.count_all, ip_addresses_path(search: { user_id: ip.user_id, ip_addr: params[:search][:ip_addr] }) %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
21
app/views/ip_addresses/index.html.erb
Normal file
21
app/views/ip_addresses/index.html.erb
Normal file
@@ -0,0 +1,21 @@
|
||||
<div id="c-ip-addresses">
|
||||
<div id="a-index">
|
||||
<%= search_form_for(ip_addresses_path) do |f| %>
|
||||
<%= f.input :user_id, label: "User ID", input_html: { value: params[:search][:user_id] }, hint: "Separate with spaces" %>
|
||||
<%= f.input :user_name, label: "User Name", input_html: { "data-autocomplete": "user", value: params[:search][:user_name] }, hint: "Use * for wildcard" %>
|
||||
<%= f.input :ip_addr, label: "IP Address", input_html: { value: params[:search][:ip_addr] } %>
|
||||
<%= f.input :created_at, label: "Date", input_html: { value: params[:search][:created_at] } %>
|
||||
<%= f.input :model_type, label: "Source", collection: IpAddress.model_types, include_blank: true, selected: params[:search][:model_type] %>
|
||||
<%= f.input :group_by, label: "Group By", collection: [["User", "user"], ["IP Address", "ip_addr"]], include_blank: true, selected: params[:search][:group_by] %>
|
||||
<%= f.submit "Search" %>
|
||||
<% end %>
|
||||
|
||||
<% if params[:search][:group_by] == "user" %>
|
||||
<%= render "index_by_user" %>
|
||||
<% elsif params[:search][:group_by] == "ip_addr" %>
|
||||
<%= render "index_by_ip_addr" %>
|
||||
<% elsif @ip_addresses.present? %>
|
||||
<%= render "index" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -10,6 +10,16 @@
|
||||
<th>Join Date</th>
|
||||
<td><%= presenter.join_date %></td>
|
||||
</tr>
|
||||
<% if CurrentUser.is_moderator? %>
|
||||
<tr>
|
||||
<th>Last IP</th>
|
||||
<td>
|
||||
<%= link_to user.last_ip_addr, ip_addresses_path(search: { ip_addr: user.last_ip_addr }) %>
|
||||
(<%= link_to "users", ip_addresses_path(search: { ip_addr: user.last_ip_addr, group_by: "user" }) %>,
|
||||
<%= link_to "IPs", ip_addresses_path(search: { user_id: user.id, group_by: "ip_addr" }) %>)
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
|
||||
<tr>
|
||||
<th>Inviter</th>
|
||||
|
||||
Reference in New Issue
Block a user