users: track logins, signups, and other user events.
Add tracking of certain important user actions. These events include: * Logins * Logouts * Failed login attempts * Account creations * Account deletions * Password reset requests * Password changes * Email address changes This is similar to the mod actions log, except for account activity related to a single user. The information tracked includes the user, the event type (login, logout, etc), the timestamp, the user's IP address, IP geolocation information, the user's browser user agent, and the user's session ID from their session cookie. This information is visible to mods only. This is done with three models. The UserEvent model tracks the event type (login, logout, password change, etc) and the user. The UserEvent is tied to a UserSession, which contains the user's IP address and browser metadata. Finally, the IpGeolocation model contains the geolocation information for IPs, including the city, country, ISP, and whether the IP is a proxy. This tracking will be used for a few purposes: * Letting users view their account history, to detect things like logins from unrecognized IPs, failed logins attempts, password changes, etc. * Rate limiting failed login attempts. * Detecting sockpuppet accounts using their login history. * Detecting unauthorized account sharing.
This commit is contained in:
@@ -26,6 +26,7 @@ class EmailsController < ApplicationController
|
||||
@user = authorize User.find(params[:user_id]), policy_class: EmailAddressPolicy
|
||||
|
||||
if @user.authenticate_password(params[:user][:password])
|
||||
UserEvent.build_from_request(@user, :email_change, request)
|
||||
@user.update(email_address_attributes: { address: params[:user][:email] })
|
||||
else
|
||||
@user.errors.add(:base, "Password was incorrect")
|
||||
|
||||
@@ -17,7 +17,7 @@ class IpAddressesController < ApplicationController
|
||||
|
||||
def show
|
||||
@ip_address = authorize IpAddress.new(ip_addr: params[:id])
|
||||
@ip_info = @ip_address.lookup.info
|
||||
@ip_info = @ip_address.lookup.response
|
||||
respond_with(@ip_info)
|
||||
end
|
||||
end
|
||||
|
||||
9
app/controllers/ip_geolocations_controller.rb
Normal file
9
app/controllers/ip_geolocations_controller.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
class IpGeolocationsController < ApplicationController
|
||||
respond_to :html, :json, :xml
|
||||
|
||||
def index
|
||||
@ip_geolocations = authorize IpGeolocation.visible(CurrentUser.user).paginated_search(params, count_pages: true)
|
||||
|
||||
respond_with(@ip_geolocations)
|
||||
end
|
||||
end
|
||||
@@ -7,7 +7,7 @@ module Maintenance
|
||||
end
|
||||
|
||||
def destroy
|
||||
deletion = UserDeletion.new(CurrentUser.user, params.dig(:user, :password))
|
||||
deletion = UserDeletion.new(CurrentUser.user, params.dig(:user, :password), request)
|
||||
deletion.delete!
|
||||
|
||||
if deletion.errors.none?
|
||||
|
||||
@@ -9,6 +9,7 @@ class PasswordResetsController < ApplicationController
|
||||
redirect_to password_reset_path
|
||||
elsif @user.can_receive_email?(require_verification: false)
|
||||
UserMailer.password_reset(@user).deliver_later
|
||||
UserEvent.create_from_request!(@user, :password_reset, request)
|
||||
flash[:notice] = "Password reset email sent. Check your email"
|
||||
respond_with(@user, location: new_session_path)
|
||||
else
|
||||
|
||||
@@ -10,6 +10,7 @@ class PasswordsController < ApplicationController
|
||||
@user = authorize User.find(params[:user_id]), policy_class: PasswordPolicy
|
||||
|
||||
if @user.authenticate_password(params[:user][:old_password]) || @user.authenticate_login_key(params[:user][:signed_user_id]) || CurrentUser.user.is_owner?
|
||||
UserEvent.build_from_request(@user, :password_change, request)
|
||||
@user.update(password: params[:user][:password], password_confirmation: params[:user][:password_confirmation])
|
||||
else
|
||||
@user.errors.add(:base, "Incorrect password")
|
||||
|
||||
@@ -20,7 +20,7 @@ class SessionsController < ApplicationController
|
||||
end
|
||||
|
||||
def destroy
|
||||
session.delete(:user_id)
|
||||
SessionLoader.new(request).logout
|
||||
redirect_to(posts_path, :notice => "You are now logged out")
|
||||
end
|
||||
|
||||
|
||||
10
app/controllers/user_events_controller.rb
Normal file
10
app/controllers/user_events_controller.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
class UserEventsController < ApplicationController
|
||||
respond_to :html, :json, :xml
|
||||
|
||||
def index
|
||||
@user_events = authorize UserEvent.visible(CurrentUser.user).paginated_search(params, count_pages: true)
|
||||
@user_events = @user_events.includes(:user, user_session: [:ip_geolocation]) if request.format.html?
|
||||
|
||||
respond_with(@user_events)
|
||||
end
|
||||
end
|
||||
9
app/controllers/user_sessions_controller.rb
Normal file
9
app/controllers/user_sessions_controller.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
class UserSessionsController < ApplicationController
|
||||
respond_to :html, :json, :xml
|
||||
|
||||
def index
|
||||
@user_sessions = authorize UserSession.visible(CurrentUser.user).paginated_search(params, count_pages: true)
|
||||
|
||||
respond_with(@user_sessions)
|
||||
end
|
||||
end
|
||||
@@ -70,6 +70,8 @@ class UsersController < ApplicationController
|
||||
password_confirmation: params[:user][:password_confirmation]
|
||||
)
|
||||
|
||||
UserEvent.build_from_request(@user, :user_creation, request)
|
||||
|
||||
if params[:user][:email].present?
|
||||
@user.email_address = EmailAddress.new(address: params[:user][:email])
|
||||
end
|
||||
|
||||
@@ -193,7 +193,7 @@ module Searchable
|
||||
search_text_attribute(name, params)
|
||||
when :boolean
|
||||
search_boolean_attribute(name, params)
|
||||
when :integer, :datetime
|
||||
when :integer, :float, :datetime
|
||||
search_numeric_attribute(name, params)
|
||||
when :inet
|
||||
search_inet_attribute(name, params)
|
||||
|
||||
@@ -9,23 +9,51 @@ class IpLookup
|
||||
Danbooru.config.ip_registry_api_key.present?
|
||||
end
|
||||
|
||||
def initialize(ip_addr, api_key: Danbooru.config.ip_registry_api_key, cache_duration: 1.day)
|
||||
@ip_addr = ip_addr
|
||||
def initialize(ip_addr, api_key: Danbooru.config.ip_registry_api_key, cache_duration: 3.days)
|
||||
@ip_addr = IPAddress.parse(ip_addr.to_s)
|
||||
@api_key = api_key
|
||||
@cache_duration = cache_duration
|
||||
end
|
||||
|
||||
def info
|
||||
def ip_info
|
||||
return {} if response.blank?
|
||||
|
||||
{
|
||||
ip_addr: ip_addr.to_s,
|
||||
network: response.dig(:connection, :route),
|
||||
asn: response.dig(:connection, :asn),
|
||||
is_proxy: is_proxy?,
|
||||
latitude: response.dig(:location, :latitude),
|
||||
longitude: response.dig(:location, :longitude),
|
||||
organization: response.dig(:connection, :organization),
|
||||
time_zone: response.dig(:time_zone, :id),
|
||||
continent: response.dig(:location, :continent, :code),
|
||||
country: response.dig(:location, :country, :code),
|
||||
region: response.dig(:location, :region, :code),
|
||||
city: response.dig(:location, :city),
|
||||
carrier: response.dig(:carrier, :name),
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
def response
|
||||
return {} if api_key.blank?
|
||||
response = Danbooru::Http.cache(cache_duration).get("https://api.ipregistry.co/#{ip_addr}?key=#{api_key}")
|
||||
response = Danbooru::Http.cache(cache_duration).get("https://api.ipregistry.co/#{ip_addr.to_s}?key=#{api_key}")
|
||||
return {} if response.status != 200
|
||||
json = response.parse.deep_symbolize_keys.with_indifferent_access
|
||||
json
|
||||
end
|
||||
|
||||
def is_proxy?
|
||||
info[:security].present? && info[:security].values.any?
|
||||
def is_local?
|
||||
if ip_addr.ipv4?
|
||||
ip_addr.loopback? || ip_addr.link_local? || ip_addr.private?
|
||||
elsif ip_addr.ipv6?
|
||||
ip_addr.loopback? || ip_addr.link_local? || ip_addr.unique_local?
|
||||
end
|
||||
end
|
||||
|
||||
memoize :info, :is_proxy?
|
||||
def is_proxy?
|
||||
response[:security].present? && response[:security].values.any?
|
||||
end
|
||||
|
||||
memoize :response, :is_proxy?
|
||||
end
|
||||
|
||||
@@ -10,12 +10,28 @@ class SessionLoader
|
||||
end
|
||||
|
||||
def login(name, password)
|
||||
user = User.find_by_name(name)&.authenticate_password(password)
|
||||
return nil unless user
|
||||
user = User.find_by_name(name)
|
||||
|
||||
session[:user_id] = user.id
|
||||
user.update_column(:last_ip_addr, request.remote_ip)
|
||||
user
|
||||
if user.present? && user.authenticate_password(password)
|
||||
session[:user_id] = user.id
|
||||
|
||||
UserEvent.build_from_request(user, :login, request)
|
||||
user.last_logged_in_at = Time.now
|
||||
user.last_ip_addr = request.remote_ip
|
||||
user.save!
|
||||
|
||||
user
|
||||
elsif user.nil?
|
||||
nil # username incorrect
|
||||
else
|
||||
UserEvent.create_from_request!(user, :failed_login, request)
|
||||
nil # password incorrect
|
||||
end
|
||||
end
|
||||
|
||||
def logout
|
||||
session.delete(:user_id)
|
||||
UserEvent.create_from_request!(CurrentUser.user, :logout, request)
|
||||
end
|
||||
|
||||
def load
|
||||
@@ -76,6 +92,7 @@ class SessionLoader
|
||||
CurrentUser.user = user
|
||||
end
|
||||
|
||||
# XXX use rails 6.1 signed ids (https://github.com/rails/rails/blob/6-1-stable/activerecord/CHANGELOG.md)
|
||||
def load_param_user(signed_user_id)
|
||||
session[:user_id] = Danbooru::MessageVerifier.new(:login).verify(signed_user_id)
|
||||
load_session_user
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
class UserDeletion
|
||||
include ActiveModel::Validations
|
||||
|
||||
attr_reader :user, :password
|
||||
attr_reader :user, :password, :request
|
||||
|
||||
validate :validate_deletion
|
||||
|
||||
def initialize(user, password)
|
||||
def initialize(user, password, request)
|
||||
@user = user
|
||||
@password = password
|
||||
@request = request
|
||||
end
|
||||
|
||||
def delete!
|
||||
return false if invalid?
|
||||
|
||||
clear_user_settings
|
||||
remove_favorites
|
||||
clear_saved_searches
|
||||
rename
|
||||
reset_password
|
||||
create_mod_action
|
||||
create_user_event
|
||||
user
|
||||
end
|
||||
|
||||
@@ -27,6 +30,10 @@ class UserDeletion
|
||||
ModAction.log("user ##{user.id} deleted", :user_delete)
|
||||
end
|
||||
|
||||
def create_user_event
|
||||
UserEvent.create_from_request!(user, :user_deletion, request)
|
||||
end
|
||||
|
||||
def clear_saved_searches
|
||||
SavedSearch.where(user_id: user.id).destroy_all
|
||||
end
|
||||
|
||||
@@ -33,11 +33,7 @@ class UserVerifier
|
||||
end
|
||||
|
||||
def is_local_ip?
|
||||
if ip_address.ipv4?
|
||||
ip_address.loopback? || ip_address.link_local? || ip_address.private?
|
||||
elsif ip_address.ipv6?
|
||||
ip_address.loopback? || ip_address.link_local? || ip_address.unique_local?
|
||||
end
|
||||
IpLookup.new(ip_address).is_local?
|
||||
end
|
||||
|
||||
def is_logged_in?
|
||||
|
||||
29
app/models/ip_geolocation.rb
Normal file
29
app/models/ip_geolocation.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
# An IpGeolocation contains metadata associated with an IP address, primarily geolocation data.
|
||||
|
||||
class IpGeolocation < ApplicationRecord
|
||||
has_many :user_sessions, foreign_key: :ip_addr, primary_key: :ip_addr
|
||||
|
||||
def self.visible(user)
|
||||
if user.is_moderator?
|
||||
all
|
||||
else
|
||||
none
|
||||
end
|
||||
end
|
||||
|
||||
def self.search(params)
|
||||
q = search_attributes(params, :id, :created_at, :updated_at, :ip_addr, :network, :asn, :is_proxy, :latitude, :longitude, :organization, :time_zone, :continent, :country, :region, :city, :carrier)
|
||||
q = q.apply_default_order(params)
|
||||
q
|
||||
end
|
||||
|
||||
def self.create_or_update!(ip_addr)
|
||||
ip_lookup = IpLookup.new(ip_addr)
|
||||
return nil if ip_lookup.is_local?
|
||||
return nil if ip_lookup.ip_info.blank?
|
||||
|
||||
ip_geolocation = IpGeolocation.create_with(**ip_lookup.ip_info).create_or_find_by!(ip_addr: ip_addr)
|
||||
ip_geolocation.update!(**ip_lookup.ip_info)
|
||||
ip_geolocation
|
||||
end
|
||||
end
|
||||
@@ -102,6 +102,7 @@ class User < ApplicationRecord
|
||||
has_many :bans, -> {order("bans.id desc")}
|
||||
has_many :received_upgrades, class_name: "UserUpgrade", foreign_key: :recipient_id, dependent: :destroy
|
||||
has_many :purchased_upgrades, class_name: "UserUpgrade", foreign_key: :purchaser_id, dependent: :destroy
|
||||
has_many :user_events, dependent: :destroy
|
||||
has_one :recent_ban, -> {order("bans.id desc")}, :class_name => "Ban"
|
||||
|
||||
has_one :api_key
|
||||
|
||||
54
app/models/user_event.rb
Normal file
54
app/models/user_event.rb
Normal file
@@ -0,0 +1,54 @@
|
||||
# A UserEvent is used to track important events related to a user's account,
|
||||
# such as signups, logins, password changes, etc. A UserEvent is associated
|
||||
# with a UserSession, which contains the IP and browser information associated
|
||||
# with the event.
|
||||
|
||||
class UserEvent < ApplicationRecord
|
||||
belongs_to :user
|
||||
belongs_to :user_session
|
||||
|
||||
enum category: {
|
||||
login: 0,
|
||||
failed_login: 50,
|
||||
logout: 100,
|
||||
user_creation: 200,
|
||||
user_deletion: 300,
|
||||
password_reset: 400,
|
||||
password_change: 500,
|
||||
email_change: 600,
|
||||
}
|
||||
|
||||
delegate :session_id, :ip_addr, :ip_geolocation, to: :user_session
|
||||
delegate :country, :city, :is_proxy?, to: :ip_geolocation, allow_nil: true
|
||||
|
||||
def self.visible(user)
|
||||
if user.is_moderator?
|
||||
all
|
||||
else
|
||||
where(user: user)
|
||||
end
|
||||
end
|
||||
|
||||
def self.search(params)
|
||||
q = search_attributes(params, :id, :created_at, :updated_at, :category, :user, :user_session)
|
||||
q = q.apply_default_order(params)
|
||||
q
|
||||
end
|
||||
|
||||
concerning :ConstructorMethods do
|
||||
class_methods do
|
||||
# Build an event but don't save it yet. The caller is expected to update the user, which will save the event.
|
||||
def build_from_request(user, category, request)
|
||||
ip_addr = request.remote_ip
|
||||
IpGeolocation.create_or_update!(ip_addr)
|
||||
user_session = UserSession.new(session_id: request.session[:session_id], ip_addr: ip_addr, user_agent: request.user_agent)
|
||||
|
||||
user.user_events.build(user: user, category: category, user_session: user_session)
|
||||
end
|
||||
|
||||
def create_from_request!(...)
|
||||
build_from_request(...).save!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
21
app/models/user_session.rb
Normal file
21
app/models/user_session.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
# A UserSession contains browser and IP metadata associated with a UserEvent. This
|
||||
# includes the user's session ID from their session cookie, their IP address,
|
||||
# and their browser user agent. This is used to track logins and other events.
|
||||
|
||||
class UserSession < ApplicationRecord
|
||||
belongs_to :ip_geolocation, foreign_key: :ip_addr, primary_key: :ip_addr, optional: true
|
||||
|
||||
def self.visible(user)
|
||||
if user.is_moderator?
|
||||
all
|
||||
else
|
||||
none
|
||||
end
|
||||
end
|
||||
|
||||
def self.search(params)
|
||||
q = search_attributes(params, :id, :created_at, :updated_at, :session_id, :user_agent, :ip_addr, :ip_geolocation)
|
||||
q = q.apply_default_order(params)
|
||||
q
|
||||
end
|
||||
end
|
||||
5
app/policies/ip_geolocation_policy.rb
Normal file
5
app/policies/ip_geolocation_policy.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class IpGeolocationPolicy < ApplicationPolicy
|
||||
def index?
|
||||
user.is_moderator?
|
||||
end
|
||||
end
|
||||
5
app/policies/user_event_policy.rb
Normal file
5
app/policies/user_event_policy.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class UserEventPolicy < ApplicationPolicy
|
||||
def index?
|
||||
user.is_moderator?
|
||||
end
|
||||
end
|
||||
5
app/policies/user_session_policy.rb
Normal file
5
app/policies/user_session_policy.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class UserSessionPolicy < ApplicationPolicy
|
||||
def index?
|
||||
user.is_moderator?
|
||||
end
|
||||
end
|
||||
43
app/views/ip_geolocations/index.html.erb
Normal file
43
app/views/ip_geolocations/index.html.erb
Normal file
@@ -0,0 +1,43 @@
|
||||
<div id="c-ip-geolocations">
|
||||
<div id="a-index">
|
||||
<%= search_form_for(ip_geolocations_path) do |f| %>
|
||||
<%= f.input :ip_addr, label: "IP Address", input_html: { value: params[:search][:ip_addr] } %>
|
||||
<%= f.input :asn, input_html: { value: params[:search][:asn] } %>
|
||||
<%= f.input :continent, input_html: { value: params[:search][:continent] } %>
|
||||
<%= f.input :country, as: :string, input_html: { value: params[:search][:country] } %>
|
||||
<%= f.input :region, input_html: { value: params[:search][:region] } %>
|
||||
<%= f.input :city, input_html: { value: params[:search][:city] } %>
|
||||
<%= f.input :is_proxy, label: "Proxy?", as: :select, include_blank: true, selected: params[:search][:is_proxy] %>
|
||||
<%= f.submit "Search" %>
|
||||
<% end %>
|
||||
|
||||
<%= table_for @ip_geolocations, class: "striped autofit" do |t| %>
|
||||
<% t.column "IP Address" do |ip_geolocation| %>
|
||||
<%= ip_geolocation.ip_addr %>
|
||||
<% end %>
|
||||
|
||||
<% t.column :network %>
|
||||
<% t.column :asn %>
|
||||
<% t.column :organization %>
|
||||
<% t.column :is_proxy %>
|
||||
<% t.column :continent %>
|
||||
<% t.column :country %>
|
||||
<% t.column :region %>
|
||||
<% t.column :city %>
|
||||
|
||||
<% t.column "Updated" do |ip_geolocation| %>
|
||||
<%= time_ago_in_words_tagged(ip_geolocation.updated_at) %>
|
||||
<% end %>
|
||||
|
||||
<% t.column "Created" do |ip_geolocation| %>
|
||||
<%= time_ago_in_words_tagged(ip_geolocation.created_at) %>
|
||||
<% end %>
|
||||
|
||||
<% t.column column: "control" do |ip_geolocation| %>
|
||||
<%= external_link_to "https://ipinfo.io/#{ip_geolocation.ip_addr}", "IP Info" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= numbered_paginator(@ip_geolocations) %>
|
||||
</div>
|
||||
</div>
|
||||
66
app/views/user_events/index.html.erb
Normal file
66
app/views/user_events/index.html.erb
Normal file
@@ -0,0 +1,66 @@
|
||||
<div id="c-user-events">
|
||||
<div id="a-index">
|
||||
<%= search_form_for(user_events_path) do |f| %>
|
||||
<%= f.input :user_name, label: "User", input_html: { value: params[:search][:user_name], "data-autocomplete": "user" } %>
|
||||
<%= f.simple_fields_for :user_session do |f1| %>
|
||||
<%= f1.input :ip_addr, label: "IP Address", input_html: { value: params.dig(:search, :user_session, :ip_addr) } %>
|
||||
<%= f1.input :session_id, label: "Session", input_html: { value: params.dig(:search, :user_session, :session_id) } %>
|
||||
<%= f1.simple_fields_for :ip_geolocation do |f2| %>
|
||||
<%= f2.input :country, as: :string, input_html: { value: params.dig(:search, :user_session, :ip_geolocation, :country) } %>
|
||||
<%= f2.input :city, input_html: { value: params.dig(:search, :user_session, :ip_geolocation, :city) } %>
|
||||
<%= f2.input :is_proxy, label: "Proxy?", as: :select, include_blank: true, selected: params.dig(:search, :user_session, :ip_geolocation, :is_proxy) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= f.input :category, collection: UserEvent.categories.transform_keys(&:humanize), include_blank: true, selected: params[:search][:category] %>
|
||||
<%= f.submit "Search" %>
|
||||
<% end %>
|
||||
|
||||
<%= table_for @user_events, class: "striped autofit" do |t| %>
|
||||
<% t.column "User" do |user_event| %>
|
||||
<%= link_to_user user_event.user %>
|
||||
<%= link_to "»", user_events_path(search: { user_name: user_event.user.name }) %>
|
||||
<% end %>
|
||||
|
||||
<% t.column :category do |user_event| %>
|
||||
<%= link_to user_event.category.humanize, user_events_path(search: { category: UserEvent.categories[user_event.category] }) %>
|
||||
<% end %>
|
||||
|
||||
<% t.column "Session" do |user_event| %>
|
||||
<%= link_to user_event.session_id, user_events_path(search: { user_session: { session_id: user_event.session_id }}) %>
|
||||
<% end %>
|
||||
|
||||
<% t.column "IP Address" do |user_event| %>
|
||||
<%= link_to user_event.ip_addr, user_events_path(search: { user_session: { ip_addr: user_event.ip_addr }}) %>
|
||||
<%= link_to "»", ip_geolocations_path(search: { ip_addr: user_event.ip_addr }) %>
|
||||
<% end %>
|
||||
|
||||
<% t.column "Country" do |user_event| %>
|
||||
<% if user_event.country.present? %>
|
||||
<%= link_to user_event.country, user_events_path(search: { user_session: { ip_geolocation: { country: user_event.country }}}) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% t.column "City" do |user_event| %>
|
||||
<% if user_event.city.present? %>
|
||||
<%= link_to user_event.city, user_events_path(search: { user_session: { ip_geolocation: { city: user_event.city }}}) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% t.column "Proxy?" do |user_event| %>
|
||||
<% if user_event.is_proxy?.present? %>
|
||||
<%= link_to user_event.is_proxy? ? "Yes" : "No", user_events_path(search: { user_session: { ip_geolocation: { is_proxy: user_event.is_proxy? }}}) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% t.column "Created" do |user_event| %>
|
||||
<%= time_ago_in_words_tagged(user_event.created_at) %>
|
||||
<% end %>
|
||||
|
||||
<% t.column column: "control" do |user_event| %>
|
||||
<%= external_link_to "https://ipinfo.io/#{user_event.ip_addr}", "IP Info" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= numbered_paginator(@user_events) %>
|
||||
</div>
|
||||
</div>
|
||||
31
app/views/user_sessions/index.html.erb
Normal file
31
app/views/user_sessions/index.html.erb
Normal file
@@ -0,0 +1,31 @@
|
||||
<div id="c-user-sessions">
|
||||
<div id="a-index">
|
||||
<%= search_form_for(user_sessions_path) do |f| %>
|
||||
<%= f.input :session_id, label: "Session", input_html: { value: params[:search][:session_id] } %>
|
||||
<%= f.input :ip_addr, label: "IP Address", input_html: { value: params[:search][:ip_addr] } %>
|
||||
<%= f.submit "Search" %>
|
||||
<% end %>
|
||||
|
||||
<%= table_for @user_sessions, class: "striped autofit" do |t| %>
|
||||
<% t.column "Session" do |user_session| %>
|
||||
<%= link_to user_session.session_id, user_sessions_path(search: { session_id: user_session.session_id }) %>
|
||||
<%= link_to "»", user_events_path(search: { user_session: { session_id: user_session.session_id }}) %>
|
||||
<% end %>
|
||||
|
||||
<% t.column "IP Address" do |user_session| %>
|
||||
<%= link_to user_session.ip_addr, user_sessions_path(search: { ip_addr: user_session.ip_addr }) %>
|
||||
<%= link_to "»", user_events_path(search: { user_session: { ip_addr: user_session.ip_addr }}) %>
|
||||
<% end %>
|
||||
|
||||
<% t.column "Browser" do |user_session| %>
|
||||
<%= user_session.user_agent %>
|
||||
<% end %>
|
||||
|
||||
<% t.column "Created" do |user_session| %>
|
||||
<%= time_ago_in_words_tagged(user_session.created_at) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= numbered_paginator(@user_sessions) %>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user