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:
evazion
2021-01-07 20:06:59 -06:00
parent 94e125709c
commit 65adcd09c2
39 changed files with 856 additions and 28 deletions

View File

@@ -26,6 +26,7 @@ class EmailsController < ApplicationController
@user = authorize User.find(params[:user_id]), policy_class: EmailAddressPolicy @user = authorize User.find(params[:user_id]), policy_class: EmailAddressPolicy
if @user.authenticate_password(params[:user][:password]) if @user.authenticate_password(params[:user][:password])
UserEvent.build_from_request(@user, :email_change, request)
@user.update(email_address_attributes: { address: params[:user][:email] }) @user.update(email_address_attributes: { address: params[:user][:email] })
else else
@user.errors.add(:base, "Password was incorrect") @user.errors.add(:base, "Password was incorrect")

View File

@@ -17,7 +17,7 @@ class IpAddressesController < ApplicationController
def show def show
@ip_address = authorize IpAddress.new(ip_addr: params[:id]) @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) respond_with(@ip_info)
end end
end end

View 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

View File

@@ -7,7 +7,7 @@ module Maintenance
end end
def destroy def destroy
deletion = UserDeletion.new(CurrentUser.user, params.dig(:user, :password)) deletion = UserDeletion.new(CurrentUser.user, params.dig(:user, :password), request)
deletion.delete! deletion.delete!
if deletion.errors.none? if deletion.errors.none?

View File

@@ -9,6 +9,7 @@ class PasswordResetsController < ApplicationController
redirect_to password_reset_path redirect_to password_reset_path
elsif @user.can_receive_email?(require_verification: false) elsif @user.can_receive_email?(require_verification: false)
UserMailer.password_reset(@user).deliver_later UserMailer.password_reset(@user).deliver_later
UserEvent.create_from_request!(@user, :password_reset, request)
flash[:notice] = "Password reset email sent. Check your email" flash[:notice] = "Password reset email sent. Check your email"
respond_with(@user, location: new_session_path) respond_with(@user, location: new_session_path)
else else

View File

@@ -10,6 +10,7 @@ class PasswordsController < ApplicationController
@user = authorize User.find(params[:user_id]), policy_class: PasswordPolicy @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? 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]) @user.update(password: params[:user][:password], password_confirmation: params[:user][:password_confirmation])
else else
@user.errors.add(:base, "Incorrect password") @user.errors.add(:base, "Incorrect password")

View File

@@ -20,7 +20,7 @@ class SessionsController < ApplicationController
end end
def destroy def destroy
session.delete(:user_id) SessionLoader.new(request).logout
redirect_to(posts_path, :notice => "You are now logged out") redirect_to(posts_path, :notice => "You are now logged out")
end end

View 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

View 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

View File

@@ -70,6 +70,8 @@ class UsersController < ApplicationController
password_confirmation: params[:user][:password_confirmation] password_confirmation: params[:user][:password_confirmation]
) )
UserEvent.build_from_request(@user, :user_creation, request)
if params[:user][:email].present? if params[:user][:email].present?
@user.email_address = EmailAddress.new(address: params[:user][:email]) @user.email_address = EmailAddress.new(address: params[:user][:email])
end end

View File

@@ -193,7 +193,7 @@ module Searchable
search_text_attribute(name, params) search_text_attribute(name, params)
when :boolean when :boolean
search_boolean_attribute(name, params) search_boolean_attribute(name, params)
when :integer, :datetime when :integer, :float, :datetime
search_numeric_attribute(name, params) search_numeric_attribute(name, params)
when :inet when :inet
search_inet_attribute(name, params) search_inet_attribute(name, params)

View File

@@ -9,23 +9,51 @@ class IpLookup
Danbooru.config.ip_registry_api_key.present? Danbooru.config.ip_registry_api_key.present?
end end
def initialize(ip_addr, api_key: Danbooru.config.ip_registry_api_key, cache_duration: 1.day) def initialize(ip_addr, api_key: Danbooru.config.ip_registry_api_key, cache_duration: 3.days)
@ip_addr = ip_addr @ip_addr = IPAddress.parse(ip_addr.to_s)
@api_key = api_key @api_key = api_key
@cache_duration = cache_duration @cache_duration = cache_duration
end 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? 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 return {} if response.status != 200
json = response.parse.deep_symbolize_keys.with_indifferent_access json = response.parse.deep_symbolize_keys.with_indifferent_access
json json
end end
def is_proxy? def is_local?
info[:security].present? && info[:security].values.any? 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 end
memoize :info, :is_proxy? def is_proxy?
response[:security].present? && response[:security].values.any?
end
memoize :response, :is_proxy?
end end

View File

@@ -10,12 +10,28 @@ class SessionLoader
end end
def login(name, password) def login(name, password)
user = User.find_by_name(name)&.authenticate_password(password) user = User.find_by_name(name)
return nil unless user
session[:user_id] = user.id if user.present? && user.authenticate_password(password)
user.update_column(:last_ip_addr, request.remote_ip) session[:user_id] = user.id
user
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 end
def load def load
@@ -76,6 +92,7 @@ class SessionLoader
CurrentUser.user = user CurrentUser.user = user
end 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) def load_param_user(signed_user_id)
session[:user_id] = Danbooru::MessageVerifier.new(:login).verify(signed_user_id) session[:user_id] = Danbooru::MessageVerifier.new(:login).verify(signed_user_id)
load_session_user load_session_user

View File

@@ -1,23 +1,26 @@
class UserDeletion class UserDeletion
include ActiveModel::Validations include ActiveModel::Validations
attr_reader :user, :password attr_reader :user, :password, :request
validate :validate_deletion validate :validate_deletion
def initialize(user, password) def initialize(user, password, request)
@user = user @user = user
@password = password @password = password
@request = request
end end
def delete! def delete!
return false if invalid? return false if invalid?
clear_user_settings clear_user_settings
remove_favorites remove_favorites
clear_saved_searches clear_saved_searches
rename rename
reset_password reset_password
create_mod_action create_mod_action
create_user_event
user user
end end
@@ -27,6 +30,10 @@ class UserDeletion
ModAction.log("user ##{user.id} deleted", :user_delete) ModAction.log("user ##{user.id} deleted", :user_delete)
end end
def create_user_event
UserEvent.create_from_request!(user, :user_deletion, request)
end
def clear_saved_searches def clear_saved_searches
SavedSearch.where(user_id: user.id).destroy_all SavedSearch.where(user_id: user.id).destroy_all
end end

View File

@@ -33,11 +33,7 @@ class UserVerifier
end end
def is_local_ip? def is_local_ip?
if ip_address.ipv4? IpLookup.new(ip_address).is_local?
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
end end
def is_logged_in? def is_logged_in?

View 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

View File

@@ -102,6 +102,7 @@ class User < ApplicationRecord
has_many :bans, -> {order("bans.id desc")} has_many :bans, -> {order("bans.id desc")}
has_many :received_upgrades, class_name: "UserUpgrade", foreign_key: :recipient_id, dependent: :destroy 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 :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 :recent_ban, -> {order("bans.id desc")}, :class_name => "Ban"
has_one :api_key has_one :api_key

54
app/models/user_event.rb Normal file
View 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

View 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

View File

@@ -0,0 +1,5 @@
class IpGeolocationPolicy < ApplicationPolicy
def index?
user.is_moderator?
end
end

View File

@@ -0,0 +1,5 @@
class UserEventPolicy < ApplicationPolicy
def index?
user.is_moderator?
end
end

View File

@@ -0,0 +1,5 @@
class UserSessionPolicy < ApplicationPolicy
def index?
user.is_moderator?
end
end

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

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

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

View File

@@ -140,6 +140,7 @@ Rails.application.routes.draw do
resources :forum_topic_visits, only: [:index] resources :forum_topic_visits, only: [:index]
resources :ip_bans, only: [:index, :new, :create, :update] resources :ip_bans, only: [:index, :new, :create, :update]
resources :ip_addresses, only: [:show, :index], id: /.+?(?=\.json|\.xml|\.html)|.+/ resources :ip_addresses, only: [:show, :index], id: /.+?(?=\.json|\.xml|\.html)|.+/
resources :ip_geolocations, only: [:index]
resource :iqdb_queries, :only => [:show, :create] do resource :iqdb_queries, :only => [:show, :create] do
collection do collection do
get :preview get :preview
@@ -260,7 +261,9 @@ Rails.application.routes.draw do
get :payment, on: :member get :payment, on: :member
put :refund, on: :member put :refund, on: :member
end end
resources :user_events, only: [:index]
resources :user_feedbacks, except: [:destroy] resources :user_feedbacks, except: [:destroy]
resources :user_sessions, only: [:index]
resources :user_name_change_requests, only: [:new, :create, :show, :index] resources :user_name_change_requests, only: [:new, :create, :show, :index]
resources :webhooks do resources :webhooks do
post :receive, on: :collection post :receive, on: :collection

View File

@@ -0,0 +1,21 @@
class CreateIpGeolocations < ActiveRecord::Migration[6.1]
def change
create_table :ip_geolocations do |t|
t.timestamps null: false, index: true
t.inet :ip_addr, null: false, index: { unique: true }
t.inet :network, index: true
t.integer :asn, index: true
t.boolean :is_proxy, null: false, index: true
t.float :latitude, index: true
t.float :longitude, index: true
t.string :organization, index: true
t.string :time_zone, index: true
t.string :continent, index: true
t.string :country, index: true
t.string :region, index: true
t.string :city, index: true
t.string :carrier, index: true
end
end
end

View File

@@ -0,0 +1,10 @@
class CreateUserSessions < ActiveRecord::Migration[6.1]
def change
create_table :user_sessions do |t|
t.timestamps null: false, index: true
t.inet :ip_addr, null: false, index: true
t.string :session_id, null: false, index: true
t.string :user_agent
end
end
end

View File

@@ -0,0 +1,10 @@
class CreateUserEvents < ActiveRecord::Migration[6.1]
def change
create_table :user_events do |t|
t.timestamps null: false, index: true
t.references :user, null: false, index: true
t.references :user_session, null: false, index: true
t.integer :category, null: false, index: true
end
end
end

View File

@@ -2364,6 +2364,49 @@ CREATE SEQUENCE public.ip_bans_id_seq
ALTER SEQUENCE public.ip_bans_id_seq OWNED BY public.ip_bans.id; ALTER SEQUENCE public.ip_bans_id_seq OWNED BY public.ip_bans.id;
--
-- Name: ip_geolocations; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.ip_geolocations (
id bigint NOT NULL,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL,
ip_addr inet NOT NULL,
network inet,
asn integer,
is_proxy boolean NOT NULL,
latitude double precision,
longitude double precision,
organization character varying,
time_zone character varying,
continent character varying,
country character varying,
region character varying,
city character varying,
carrier character varying
);
--
-- Name: ip_geolocations_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.ip_geolocations_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: ip_geolocations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.ip_geolocations_id_seq OWNED BY public.ip_geolocations.id;
-- --
-- Name: mod_actions; Type: TABLE; Schema: public; Owner: - -- Name: mod_actions; Type: TABLE; Schema: public; Owner: -
-- --
@@ -3038,6 +3081,39 @@ CREATE SEQUENCE public.uploads_id_seq
ALTER SEQUENCE public.uploads_id_seq OWNED BY public.uploads.id; ALTER SEQUENCE public.uploads_id_seq OWNED BY public.uploads.id;
--
-- Name: user_events; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.user_events (
id bigint NOT NULL,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL,
user_id bigint NOT NULL,
user_session_id bigint NOT NULL,
category integer NOT NULL
);
--
-- Name: user_events_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.user_events_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: user_events_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.user_events_id_seq OWNED BY public.user_events.id;
-- --
-- Name: user_feedback; Type: TABLE; Schema: public; Owner: - -- Name: user_feedback; Type: TABLE; Schema: public; Owner: -
-- --
@@ -3106,6 +3182,39 @@ CREATE SEQUENCE public.user_name_change_requests_id_seq
ALTER SEQUENCE public.user_name_change_requests_id_seq OWNED BY public.user_name_change_requests.id; ALTER SEQUENCE public.user_name_change_requests_id_seq OWNED BY public.user_name_change_requests.id;
--
-- Name: user_sessions; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.user_sessions (
id bigint NOT NULL,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL,
ip_addr inet NOT NULL,
session_id character varying NOT NULL,
user_agent character varying
);
--
-- Name: user_sessions_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.user_sessions_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: user_sessions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.user_sessions_id_seq OWNED BY public.user_sessions.id;
-- --
-- Name: user_upgrades; Type: TABLE; Schema: public; Owner: - -- Name: user_upgrades; Type: TABLE; Schema: public; Owner: -
-- --
@@ -4062,6 +4171,13 @@ ALTER TABLE ONLY public.forum_topics ALTER COLUMN id SET DEFAULT nextval('public
ALTER TABLE ONLY public.ip_bans ALTER COLUMN id SET DEFAULT nextval('public.ip_bans_id_seq'::regclass); ALTER TABLE ONLY public.ip_bans ALTER COLUMN id SET DEFAULT nextval('public.ip_bans_id_seq'::regclass);
--
-- Name: ip_geolocations id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.ip_geolocations ALTER COLUMN id SET DEFAULT nextval('public.ip_geolocations_id_seq'::regclass);
-- --
-- Name: mod_actions id; Type: DEFAULT; Schema: public; Owner: - -- Name: mod_actions id; Type: DEFAULT; Schema: public; Owner: -
-- --
@@ -4195,6 +4311,13 @@ ALTER TABLE ONLY public.tags ALTER COLUMN id SET DEFAULT nextval('public.tags_id
ALTER TABLE ONLY public.uploads ALTER COLUMN id SET DEFAULT nextval('public.uploads_id_seq'::regclass); ALTER TABLE ONLY public.uploads ALTER COLUMN id SET DEFAULT nextval('public.uploads_id_seq'::regclass);
--
-- Name: user_events id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.user_events ALTER COLUMN id SET DEFAULT nextval('public.user_events_id_seq'::regclass);
-- --
-- Name: user_feedback id; Type: DEFAULT; Schema: public; Owner: - -- Name: user_feedback id; Type: DEFAULT; Schema: public; Owner: -
-- --
@@ -4209,6 +4332,13 @@ ALTER TABLE ONLY public.user_feedback ALTER COLUMN id SET DEFAULT nextval('publi
ALTER TABLE ONLY public.user_name_change_requests ALTER COLUMN id SET DEFAULT nextval('public.user_name_change_requests_id_seq'::regclass); ALTER TABLE ONLY public.user_name_change_requests ALTER COLUMN id SET DEFAULT nextval('public.user_name_change_requests_id_seq'::regclass);
--
-- Name: user_sessions id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.user_sessions ALTER COLUMN id SET DEFAULT nextval('public.user_sessions_id_seq'::regclass);
-- --
-- Name: user_upgrades id; Type: DEFAULT; Schema: public; Owner: - -- Name: user_upgrades id; Type: DEFAULT; Schema: public; Owner: -
-- --
@@ -4414,6 +4544,13 @@ ALTER TABLE ONLY public.ip_bans
-- --
-- Name: ip_geolocations ip_geolocations_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.ip_geolocations
ADD CONSTRAINT ip_geolocations_pkey PRIMARY KEY (id);
-- Name: mod_actions mod_actions_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- Name: mod_actions mod_actions_pkey; Type: CONSTRAINT; Schema: public; Owner: -
-- --
@@ -4565,6 +4702,14 @@ ALTER TABLE ONLY public.uploads
ADD CONSTRAINT uploads_pkey PRIMARY KEY (id); ADD CONSTRAINT uploads_pkey PRIMARY KEY (id);
--
-- Name: user_events user_events_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.user_events
ADD CONSTRAINT user_events_pkey PRIMARY KEY (id);
-- --
-- Name: user_feedback user_feedback_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- Name: user_feedback user_feedback_pkey; Type: CONSTRAINT; Schema: public; Owner: -
-- --
@@ -4581,6 +4726,14 @@ ALTER TABLE ONLY public.user_name_change_requests
ADD CONSTRAINT user_name_change_requests_pkey PRIMARY KEY (id); ADD CONSTRAINT user_name_change_requests_pkey PRIMARY KEY (id);
--
-- Name: user_sessions user_sessions_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.user_sessions
ADD CONSTRAINT user_sessions_pkey PRIMARY KEY (id);
-- --
-- Name: user_upgrades user_upgrades_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- Name: user_upgrades user_upgrades_pkey; Type: CONSTRAINT; Schema: public; Owner: -
-- --
@@ -6531,6 +6684,110 @@ CREATE INDEX index_ip_bans_on_is_deleted ON public.ip_bans USING btree (is_delet
-- --
-- Name: index_ip_geolocations_on_asn; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_ip_geolocations_on_asn ON public.ip_geolocations USING btree (asn);
--
-- Name: index_ip_geolocations_on_carrier; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_ip_geolocations_on_carrier ON public.ip_geolocations USING btree (carrier);
--
-- Name: index_ip_geolocations_on_city; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_ip_geolocations_on_city ON public.ip_geolocations USING btree (city);
--
-- Name: index_ip_geolocations_on_continent; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_ip_geolocations_on_continent ON public.ip_geolocations USING btree (continent);
--
-- Name: index_ip_geolocations_on_country; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_ip_geolocations_on_country ON public.ip_geolocations USING btree (country);
--
-- Name: index_ip_geolocations_on_created_at; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_ip_geolocations_on_created_at ON public.ip_geolocations USING btree (created_at);
--
-- Name: index_ip_geolocations_on_ip_addr; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX index_ip_geolocations_on_ip_addr ON public.ip_geolocations USING btree (ip_addr);
--
-- Name: index_ip_geolocations_on_is_proxy; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_ip_geolocations_on_is_proxy ON public.ip_geolocations USING btree (is_proxy);
--
-- Name: index_ip_geolocations_on_latitude; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_ip_geolocations_on_latitude ON public.ip_geolocations USING btree (latitude);
--
-- Name: index_ip_geolocations_on_longitude; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_ip_geolocations_on_longitude ON public.ip_geolocations USING btree (longitude);
--
-- Name: index_ip_geolocations_on_network; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_ip_geolocations_on_network ON public.ip_geolocations USING btree (network);
--
-- Name: index_ip_geolocations_on_organization; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_ip_geolocations_on_organization ON public.ip_geolocations USING btree (organization);
--
-- Name: index_ip_geolocations_on_region; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_ip_geolocations_on_region ON public.ip_geolocations USING btree (region);
--
-- Name: index_ip_geolocations_on_time_zone; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_ip_geolocations_on_time_zone ON public.ip_geolocations USING btree (time_zone);
--
-- Name: index_ip_geolocations_on_updated_at; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_ip_geolocations_on_updated_at ON public.ip_geolocations USING btree (updated_at);
-- Name: index_mod_actions_on_created_at; Type: INDEX; Schema: public; Owner: - -- Name: index_mod_actions_on_created_at; Type: INDEX; Schema: public; Owner: -
-- --
@@ -7062,6 +7319,41 @@ CREATE INDEX index_uploads_on_uploader_id ON public.uploads USING btree (uploade
CREATE INDEX index_uploads_on_uploader_ip_addr ON public.uploads USING btree (uploader_ip_addr); CREATE INDEX index_uploads_on_uploader_ip_addr ON public.uploads USING btree (uploader_ip_addr);
--
-- Name: index_user_events_on_category; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_user_events_on_category ON public.user_events USING btree (category);
--
-- Name: index_user_events_on_created_at; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_user_events_on_created_at ON public.user_events USING btree (created_at);
--
-- Name: index_user_events_on_updated_at; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_user_events_on_updated_at ON public.user_events USING btree (updated_at);
--
-- Name: index_user_events_on_user_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_user_events_on_user_id ON public.user_events USING btree (user_id);
--
-- Name: index_user_events_on_user_session_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_user_events_on_user_session_id ON public.user_events USING btree (user_session_id);
-- --
-- Name: index_user_feedback_on_created_at; Type: INDEX; Schema: public; Owner: - -- Name: index_user_feedback_on_created_at; Type: INDEX; Schema: public; Owner: -
-- --
@@ -7097,6 +7389,34 @@ CREATE INDEX index_user_name_change_requests_on_original_name ON public.user_nam
CREATE INDEX index_user_name_change_requests_on_user_id ON public.user_name_change_requests USING btree (user_id); CREATE INDEX index_user_name_change_requests_on_user_id ON public.user_name_change_requests USING btree (user_id);
--
-- Name: index_user_sessions_on_created_at; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_user_sessions_on_created_at ON public.user_sessions USING btree (created_at);
--
-- Name: index_user_sessions_on_ip_addr; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_user_sessions_on_ip_addr ON public.user_sessions USING btree (ip_addr);
--
-- Name: index_user_sessions_on_session_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_user_sessions_on_session_id ON public.user_sessions USING btree (session_id);
--
-- Name: index_user_sessions_on_updated_at; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_user_sessions_on_updated_at ON public.user_sessions USING btree (updated_at);
-- --
-- Name: index_user_upgrades_on_purchaser_id; Type: INDEX; Schema: public; Owner: - -- Name: index_user_upgrades_on_purchaser_id; Type: INDEX; Schema: public; Owner: -
-- --
@@ -7526,6 +7846,9 @@ INSERT INTO "schema_migrations" (version) VALUES
('20201213052805'), ('20201213052805'),
('20201219201007'), ('20201219201007'),
('20201224101208'), ('20201224101208'),
('20210106212805'); ('20210106212805'),
('20210108030722'),
('20210108030723'),
('20210108030724');

View File

@@ -0,0 +1,5 @@
FactoryBot.define do
factory(:user_event) do
user
end
end

View File

@@ -89,6 +89,7 @@ class EmailsControllerTest < ActionDispatch::IntegrationTest
assert_equal("abc@ogres.net", @user.reload.email_address.address) assert_equal("abc@ogres.net", @user.reload.email_address.address)
assert_equal(false, @user.email_address.is_verified) assert_equal(false, @user.email_address.is_verified)
assert_enqueued_email_with UserMailer, :email_change_confirmation, args: [@user], queue: "default" assert_enqueued_email_with UserMailer, :email_change_confirmation, args: [@user], queue: "default"
assert_equal(true, @user.user_events.email_change.exists?)
end end
should "create a new address" do should "create a new address" do
@@ -102,6 +103,7 @@ class EmailsControllerTest < ActionDispatch::IntegrationTest
assert_equal("abc@ogres.net", @user.reload.email_address.address) assert_equal("abc@ogres.net", @user.reload.email_address.address)
assert_equal(false, @user.reload.email_address.is_verified) assert_equal(false, @user.reload.email_address.is_verified)
assert_enqueued_email_with UserMailer, :email_change_confirmation, args: [@user], queue: "default" assert_enqueued_email_with UserMailer, :email_change_confirmation, args: [@user], queue: "default"
assert_equal(true, @user.user_events.email_change.exists?)
end end
end end
@@ -112,6 +114,7 @@ class EmailsControllerTest < ActionDispatch::IntegrationTest
assert_response :success assert_response :success
assert_equal("bob@ogres.net", @user.reload.email_address.address) assert_equal("bob@ogres.net", @user.reload.email_address.address)
assert_no_emails assert_no_emails
assert_equal(false, @user.user_events.email_change.exists?)
end end
end end
end end

View File

@@ -18,18 +18,22 @@ module Maintenance
context "#destroy" do context "#destroy" do
should "delete the user when given the correct password" do should "delete the user when given the correct password" do
delete_auth maintenance_user_deletion_path, @user, params: { user: { password: "password" }} delete_auth maintenance_user_deletion_path, @user, params: { user: { password: "password" }}
assert_redirected_to posts_path assert_redirected_to posts_path
assert_equal(true, @user.reload.is_deleted?) assert_equal(true, @user.reload.is_deleted?)
assert_equal("Your account has been deactivated", flash[:notice]) assert_equal("Your account has been deactivated", flash[:notice])
assert_nil(session[:user_id]) assert_nil(session[:user_id])
assert_equal(true, @user.user_events.user_deletion.exists?)
end end
should "not delete the user when given an incorrect password" do should "not delete the user when given an incorrect password" do
delete_auth maintenance_user_deletion_path, @user, params: { user: { password: "hunter2" }} delete_auth maintenance_user_deletion_path, @user, params: { user: { password: "hunter2" }}
assert_redirected_to maintenance_user_deletion_path assert_redirected_to maintenance_user_deletion_path
assert_equal(false, @user.reload.is_deleted?) assert_equal(false, @user.reload.is_deleted?)
assert_equal("Password is incorrect", flash[:notice]) assert_equal("Password is incorrect", flash[:notice])
assert_equal(@user.id, session[:user_id]) assert_equal(@user.id, session[:user_id])
assert_equal(false, @user.user_events.user_deletion.exists?)
end end
end end
end end

View File

@@ -16,6 +16,7 @@ class PasswordResetsControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to new_session_path assert_redirected_to new_session_path
assert_enqueued_email_with UserMailer, :password_reset, args: [@user], queue: "default" assert_enqueued_email_with UserMailer, :password_reset, args: [@user], queue: "default"
assert_equal(true, @user.user_events.password_reset.exists?)
end end
should "should fail if the user doesn't have a verified email address" do should "should fail if the user doesn't have a verified email address" do

View File

@@ -20,6 +20,7 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to @user assert_redirected_to @user
assert_equal(false, @user.reload.authenticate_password("12345")) assert_equal(false, @user.reload.authenticate_password("12345"))
assert_equal(@user, @user.authenticate_password("abcde")) assert_equal(@user, @user.authenticate_password("abcde"))
assert_equal(true, @user.user_events.password_change.exists?)
end end
should "update the password when given a valid login key" do should "update the password when given a valid login key" do
@@ -29,6 +30,7 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to @user assert_redirected_to @user
assert_equal(false, @user.reload.authenticate_password("12345")) assert_equal(false, @user.reload.authenticate_password("12345"))
assert_equal(@user, @user.authenticate_password("abcde")) assert_equal(@user, @user.authenticate_password("abcde"))
assert_equal(true, @user.user_events.password_change.exists?)
end end
should "allow the site owner to change the password of other users" do should "allow the site owner to change the password of other users" do
@@ -55,6 +57,7 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest
assert_response :success assert_response :success
assert_equal(@user, @user.reload.authenticate_password("12345")) assert_equal(@user, @user.reload.authenticate_password("12345"))
assert_equal(false, @user.authenticate_password("abcde")) assert_equal(false, @user.authenticate_password("abcde"))
assert_equal(false, @user.user_events.password_change.exists?)
end end
should "not update the password when password confirmation fails for the new password" do should "not update the password when password confirmation fails for the new password" do

View File

@@ -20,11 +20,20 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to posts_path assert_redirected_to posts_path
assert_equal(@user.id, session[:user_id]) assert_equal(@user.id, session[:user_id])
assert_not_nil(@user.reload.last_ip_addr) assert_not_nil(@user.reload.last_ip_addr)
assert_equal(true, @user.user_events.login.exists?)
end end
should "not log the user in when given an incorrect password" do should "not log the user in when given an incorrect password" do
post session_path, params: { name: @user.name, password: "wrong"} post session_path, params: { name: @user.name, password: "wrong"}
assert_response 401
assert_nil(nil, session[:user_id])
assert_equal(true, @user.user_events.failed_login.exists?)
end
should "not log the user in when given an incorrect username" do
post session_path, params: { name: "dne", password: "password" }
assert_response 401 assert_response 401
assert_nil(nil, session[:user_id]) assert_nil(nil, session[:user_id])
end end
@@ -66,11 +75,18 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
end end
context "destroy action" do context "destroy action" do
should "clear the session" do setup do
delete_auth session_path, @user delete_auth session_path, @user
end
should "clear the session" do
assert_redirected_to posts_path assert_redirected_to posts_path
assert_nil(session[:user_id]) assert_nil(session[:user_id])
end end
should "generate a logout event" do
assert_equal(true, @user.user_events.logout.exists?)
end
end end
context "sign_out action" do context "sign_out action" do

View File

@@ -260,6 +260,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(User.last, User.last.authenticate_password("xxxxx1")) assert_equal(User.last, User.last.authenticate_password("xxxxx1"))
assert_nil(User.last.email_address) assert_nil(User.last.email_address)
assert_no_enqueued_emails assert_no_enqueued_emails
assert_equal(true, User.last.user_events.user_creation.exists?)
end end
should "create a user with a valid email" do should "create a user with a valid email" do
@@ -270,6 +271,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(User.last, User.last.authenticate_password("xxxxx1")) assert_equal(User.last, User.last.authenticate_password("xxxxx1"))
assert_equal("webmaster@danbooru.donmai.us", User.last.email_address.address) assert_equal("webmaster@danbooru.donmai.us", User.last.email_address.address)
assert_enqueued_email_with UserMailer, :welcome_user, args: [User.last], queue: "default" assert_enqueued_email_with UserMailer, :welcome_user, args: [User.last], queue: "default"
assert_equal(true, User.last.user_events.user_creation.exists?)
end end
should "not create a user with an invalid email" do should "not create a user with an invalid email" do
@@ -307,6 +309,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(true, User.last.is_member?) assert_equal(true, User.last.is_member?)
assert_equal(false, User.last.is_restricted?) assert_equal(false, User.last.is_restricted?)
assert_equal(false, User.last.requires_verification) assert_equal(false, User.last.requires_verification)
assert_equal(true, User.last.user_events.user_creation.exists?)
end end
should "mark accounts created by already logged in users as restricted" do should "mark accounts created by already logged in users as restricted" do
@@ -318,6 +321,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(false, User.last.is_member?) assert_equal(false, User.last.is_member?)
assert_equal(true, User.last.is_restricted?) assert_equal(true, User.last.is_restricted?)
assert_equal(true, User.last.requires_verification) assert_equal(true, User.last.requires_verification)
assert_equal(true, User.last.user_events.user_creation.exists?)
end end
should "mark users signing up from proxies as restricted" do should "mark users signing up from proxies as restricted" do
@@ -330,6 +334,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(false, User.last.is_member?) assert_equal(false, User.last.is_member?)
assert_equal(true, User.last.is_restricted?) assert_equal(true, User.last.is_restricted?)
assert_equal(true, User.last.requires_verification) assert_equal(true, User.last.requires_verification)
assert_equal(true, User.last.user_events.user_creation.exists?)
end end
should "mark users signing up from a partial banned IP as restricted" do should "mark users signing up from a partial banned IP as restricted" do
@@ -344,6 +349,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(true, User.last.requires_verification) assert_equal(true, User.last.requires_verification)
assert_equal(1, @ip_ban.reload.hit_count) assert_equal(1, @ip_ban.reload.hit_count)
assert(@ip_ban.last_hit_at > 1.minute.ago) assert(@ip_ban.last_hit_at > 1.minute.ago)
assert_equal(true, User.last.user_events.user_creation.exists?)
end end
should "not mark users signing up from non-proxies as restricted" do should "not mark users signing up from non-proxies as restricted" do
@@ -356,6 +362,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(true, User.last.is_member?) assert_equal(true, User.last.is_member?)
assert_equal(false, User.last.is_restricted?) assert_equal(false, User.last.is_restricted?)
assert_equal(false, User.last.requires_verification) assert_equal(false, User.last.requires_verification)
assert_equal(true, User.last.user_events.user_creation.exists?)
end end
should "mark accounts registered from an IPv4 address recently used for another account as restricted" do should "mark accounts registered from an IPv4 address recently used for another account as restricted" do
@@ -368,6 +375,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(false, User.last.is_member?) assert_equal(false, User.last.is_member?)
assert_equal(true, User.last.is_restricted?) assert_equal(true, User.last.is_restricted?)
assert_equal(true, User.last.requires_verification) assert_equal(true, User.last.requires_verification)
assert_equal(true, User.last.user_events.user_creation.exists?)
end end
should "not mark users signing up from localhost as restricted" do should "not mark users signing up from localhost as restricted" do
@@ -379,6 +387,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(true, User.last.is_member?) assert_equal(true, User.last.is_member?)
assert_equal(false, User.last.is_restricted?) assert_equal(false, User.last.is_restricted?)
assert_equal(false, User.last.requires_verification) assert_equal(false, User.last.requires_verification)
assert_equal(true, User.last.user_events.user_creation.exists?)
end end
end end
end end

View File

@@ -0,0 +1,72 @@
require 'test_helper'
class IpGeolocationTest < ActiveSupport::TestCase
context "IpGeolocation: " do
context "the create_or_update! method" do
should "create a new record if the IP record doesn't already exist" do
assert_difference("IpGeolocation.count", 1) do
IpGeolocation.create_or_update!("1.1.1.1")
end
end
should "update an existing record if the IP record already exists" do
@ip1 = IpGeolocation.create_or_update!("1.1.1.1")
@ip1.update(asn: -1)
@ip2 = IpGeolocation.create_or_update!("1.1.1.1")
assert_equal(1, IpGeolocation.count)
assert_equal(@ip1.id, @ip2.id)
assert_equal(13335, @ip1.reload.asn)
end
should "return nothing for an invalid IP" do
assert_nil(IpGeolocation.create_or_update!("0.0.0.0"))
end
should "return nothing for a local IP" do
assert_nil(IpGeolocation.create_or_update!("127.0.0.1"))
assert_nil(IpGeolocation.create_or_update!("10.0.0.1"))
assert_nil(IpGeolocation.create_or_update!("fe80::1"))
assert_nil(IpGeolocation.create_or_update!("::1"))
end
should "work for a residential IP" do
@ip = IpGeolocation.create_or_update!("2a01:0e35:2f22:e3d0::1")
assert_equal(28, @ip.network.prefix)
assert_equal(false, @ip.is_proxy?)
assert_equal(48.75919, @ip.latitude)
assert_equal(2.16969, @ip.longitude)
assert_equal("Free SAS", @ip.organization)
assert_equal("Europe/Paris", @ip.time_zone)
assert_equal("EU", @ip.continent)
assert_equal("FR", @ip.country)
assert_equal("FR-IDF", @ip.region)
assert_equal("Jouy-en-Josas", @ip.city)
assert_nil(@ip.carrier)
end
should "work for a mobile IP" do
@ip = IpGeolocation.create_or_update!("37.173.153.166")
assert_equal("Free Mobile", @ip.carrier)
end
should "work for a proxy IP" do
@ip = IpGeolocation.create_or_update!("31.214.184.59")
assert_equal("Soluciones Corporativas IP SL", @ip.organization)
assert_equal(true, @ip.is_proxy?)
end
should "work for a cloud hosting IP" do
@ip = IpGeolocation.create_or_update!("157.230.244.215")
assert_equal("DigitalOcean LLC", @ip.organization)
assert_equal(true, @ip.is_proxy?)
end
should "work for a bogon IP" do
@ip = IpGeolocation.create_or_update!("103.10.192.0")
assert_equal(true, @ip.is_proxy?)
end
end
end
end

View File

@@ -1,11 +1,18 @@
require 'test_helper' require 'test_helper'
class UserDeletionTest < ActiveSupport::TestCase class UserDeletionTest < ActiveSupport::TestCase
setup do
@request = mock
@request.stubs(:remote_ip).returns("1.1.1.1")
@request.stubs(:user_agent).returns("Firefox")
@request.stubs(:session).returns(session_id: "1234")
end
context "an invalid user deletion" do context "an invalid user deletion" do
context "for an invalid password" do context "for an invalid password" do
should "fail" do should "fail" do
@user = create(:user) @user = create(:user)
@deletion = UserDeletion.new(@user, "wrongpassword") @deletion = UserDeletion.new(@user, "wrongpassword", @request)
@deletion.delete! @deletion.delete!
assert_includes(@deletion.errors[:base], "Password is incorrect") assert_includes(@deletion.errors[:base], "Password is incorrect")
end end
@@ -14,7 +21,7 @@ class UserDeletionTest < ActiveSupport::TestCase
context "for an admin" do context "for an admin" do
should "fail" do should "fail" do
@user = create(:admin_user) @user = create(:admin_user)
@deletion = UserDeletion.new(@user, "password") @deletion = UserDeletion.new(@user, "password", @request)
@deletion.delete! @deletion.delete!
assert_includes(@deletion.errors[:base], "Admins cannot delete their account") assert_includes(@deletion.errors[:base], "Admins cannot delete their account")
end end
@@ -24,7 +31,7 @@ class UserDeletionTest < ActiveSupport::TestCase
context "a valid user deletion" do context "a valid user deletion" do
setup do setup do
@user = create(:user, name: "foo", email_address: build(:email_address)) @user = create(:user, name: "foo", email_address: build(:email_address))
@deletion = UserDeletion.new(@user, "password") @deletion = UserDeletion.new(@user, "password", @request)
end end
should "blank out the email" do should "blank out the email" do