diff --git a/app/controllers/emails_controller.rb b/app/controllers/emails_controller.rb index 6d8eb3f19..2f14ad9c3 100644 --- a/app/controllers/emails_controller.rb +++ b/app/controllers/emails_controller.rb @@ -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") diff --git a/app/controllers/ip_addresses_controller.rb b/app/controllers/ip_addresses_controller.rb index 11a592b01..e0ab7ffd4 100644 --- a/app/controllers/ip_addresses_controller.rb +++ b/app/controllers/ip_addresses_controller.rb @@ -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 diff --git a/app/controllers/ip_geolocations_controller.rb b/app/controllers/ip_geolocations_controller.rb new file mode 100644 index 000000000..a51f9fcc4 --- /dev/null +++ b/app/controllers/ip_geolocations_controller.rb @@ -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 diff --git a/app/controllers/maintenance/user/deletions_controller.rb b/app/controllers/maintenance/user/deletions_controller.rb index 627a65a73..8b85bb163 100644 --- a/app/controllers/maintenance/user/deletions_controller.rb +++ b/app/controllers/maintenance/user/deletions_controller.rb @@ -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? diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb index 06274fb0d..746f43869 100644 --- a/app/controllers/password_resets_controller.rb +++ b/app/controllers/password_resets_controller.rb @@ -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 diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index f5fcc40e0..ffec5bc2a 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -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") diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 610e00150..e95e76e2b 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -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 diff --git a/app/controllers/user_events_controller.rb b/app/controllers/user_events_controller.rb new file mode 100644 index 000000000..5348fcfc5 --- /dev/null +++ b/app/controllers/user_events_controller.rb @@ -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 diff --git a/app/controllers/user_sessions_controller.rb b/app/controllers/user_sessions_controller.rb new file mode 100644 index 000000000..49446fe8b --- /dev/null +++ b/app/controllers/user_sessions_controller.rb @@ -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 diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index a53d97ade..448f46c6f 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -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 diff --git a/app/logical/concerns/searchable.rb b/app/logical/concerns/searchable.rb index aa6e4971f..db7f7788b 100644 --- a/app/logical/concerns/searchable.rb +++ b/app/logical/concerns/searchable.rb @@ -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) diff --git a/app/logical/ip_lookup.rb b/app/logical/ip_lookup.rb index 7a6e3bf1a..f8dae9747 100644 --- a/app/logical/ip_lookup.rb +++ b/app/logical/ip_lookup.rb @@ -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 diff --git a/app/logical/session_loader.rb b/app/logical/session_loader.rb index 12f12e189..c3c4bcac1 100644 --- a/app/logical/session_loader.rb +++ b/app/logical/session_loader.rb @@ -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 diff --git a/app/logical/user_deletion.rb b/app/logical/user_deletion.rb index e39192736..d74d6ba56 100644 --- a/app/logical/user_deletion.rb +++ b/app/logical/user_deletion.rb @@ -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 diff --git a/app/logical/user_verifier.rb b/app/logical/user_verifier.rb index 3b493a51c..0e17e8446 100644 --- a/app/logical/user_verifier.rb +++ b/app/logical/user_verifier.rb @@ -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? diff --git a/app/models/ip_geolocation.rb b/app/models/ip_geolocation.rb new file mode 100644 index 000000000..266ce25b0 --- /dev/null +++ b/app/models/ip_geolocation.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index c48b5c620..5eb1fc0f0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/models/user_event.rb b/app/models/user_event.rb new file mode 100644 index 000000000..752a02398 --- /dev/null +++ b/app/models/user_event.rb @@ -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 diff --git a/app/models/user_session.rb b/app/models/user_session.rb new file mode 100644 index 000000000..9ecaaaab7 --- /dev/null +++ b/app/models/user_session.rb @@ -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 diff --git a/app/policies/ip_geolocation_policy.rb b/app/policies/ip_geolocation_policy.rb new file mode 100644 index 000000000..bd269f3ee --- /dev/null +++ b/app/policies/ip_geolocation_policy.rb @@ -0,0 +1,5 @@ +class IpGeolocationPolicy < ApplicationPolicy + def index? + user.is_moderator? + end +end diff --git a/app/policies/user_event_policy.rb b/app/policies/user_event_policy.rb new file mode 100644 index 000000000..ddc8b240e --- /dev/null +++ b/app/policies/user_event_policy.rb @@ -0,0 +1,5 @@ +class UserEventPolicy < ApplicationPolicy + def index? + user.is_moderator? + end +end diff --git a/app/policies/user_session_policy.rb b/app/policies/user_session_policy.rb new file mode 100644 index 000000000..cb84fe503 --- /dev/null +++ b/app/policies/user_session_policy.rb @@ -0,0 +1,5 @@ +class UserSessionPolicy < ApplicationPolicy + def index? + user.is_moderator? + end +end diff --git a/app/views/ip_geolocations/index.html.erb b/app/views/ip_geolocations/index.html.erb new file mode 100644 index 000000000..2c99283a7 --- /dev/null +++ b/app/views/ip_geolocations/index.html.erb @@ -0,0 +1,43 @@ +
+
+ <%= 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) %> +
+
diff --git a/app/views/user_events/index.html.erb b/app/views/user_events/index.html.erb new file mode 100644 index 000000000..c0ac5b22c --- /dev/null +++ b/app/views/user_events/index.html.erb @@ -0,0 +1,66 @@ +
+
+ <%= 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) %> +
+
diff --git a/app/views/user_sessions/index.html.erb b/app/views/user_sessions/index.html.erb new file mode 100644 index 000000000..f8b3c5af9 --- /dev/null +++ b/app/views/user_sessions/index.html.erb @@ -0,0 +1,31 @@ +
+
+ <%= 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) %> +
+
diff --git a/config/routes.rb b/config/routes.rb index 7b32daaf4..5457e8719 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -140,6 +140,7 @@ Rails.application.routes.draw do resources :forum_topic_visits, only: [:index] resources :ip_bans, only: [:index, :new, :create, :update] resources :ip_addresses, only: [:show, :index], id: /.+?(?=\.json|\.xml|\.html)|.+/ + resources :ip_geolocations, only: [:index] resource :iqdb_queries, :only => [:show, :create] do collection do get :preview @@ -260,7 +261,9 @@ Rails.application.routes.draw do get :payment, on: :member put :refund, on: :member end + resources :user_events, only: [:index] resources :user_feedbacks, except: [:destroy] + resources :user_sessions, only: [:index] resources :user_name_change_requests, only: [:new, :create, :show, :index] resources :webhooks do post :receive, on: :collection diff --git a/db/migrate/20210108030722_create_ip_geolocations.rb b/db/migrate/20210108030722_create_ip_geolocations.rb new file mode 100644 index 000000000..7a9da0176 --- /dev/null +++ b/db/migrate/20210108030722_create_ip_geolocations.rb @@ -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 diff --git a/db/migrate/20210108030723_create_user_sessions.rb b/db/migrate/20210108030723_create_user_sessions.rb new file mode 100644 index 000000000..56210cc6e --- /dev/null +++ b/db/migrate/20210108030723_create_user_sessions.rb @@ -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 diff --git a/db/migrate/20210108030724_create_user_events.rb b/db/migrate/20210108030724_create_user_events.rb new file mode 100644 index 000000000..dec3a7bdf --- /dev/null +++ b/db/migrate/20210108030724_create_user_events.rb @@ -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 diff --git a/db/structure.sql b/db/structure.sql index 2d978ad70..9508f40a1 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -2364,6 +2364,49 @@ CREATE SEQUENCE public.ip_bans_id_seq 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: - -- @@ -3038,6 +3081,39 @@ CREATE SEQUENCE public.uploads_id_seq 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: - -- @@ -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; +-- +-- 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: - -- @@ -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); +-- +-- 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: - -- @@ -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); +-- +-- 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: - -- @@ -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); +-- +-- 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: - -- @@ -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: - -- @@ -4565,6 +4702,14 @@ ALTER TABLE ONLY public.uploads 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: - -- @@ -4581,6 +4726,14 @@ ALTER TABLE ONLY public.user_name_change_requests 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: - -- @@ -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: - -- @@ -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); +-- +-- 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: - -- @@ -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); +-- +-- 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: - -- @@ -7526,6 +7846,9 @@ INSERT INTO "schema_migrations" (version) VALUES ('20201213052805'), ('20201219201007'), ('20201224101208'), -('20210106212805'); +('20210106212805'), +('20210108030722'), +('20210108030723'), +('20210108030724'); diff --git a/test/factories/user_event.rb b/test/factories/user_event.rb new file mode 100644 index 000000000..37957ea71 --- /dev/null +++ b/test/factories/user_event.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory(:user_event) do + user + end +end diff --git a/test/functional/emails_controller_test.rb b/test/functional/emails_controller_test.rb index ce6c5ae5c..ec6ad52d0 100644 --- a/test/functional/emails_controller_test.rb +++ b/test/functional/emails_controller_test.rb @@ -89,6 +89,7 @@ class EmailsControllerTest < ActionDispatch::IntegrationTest assert_equal("abc@ogres.net", @user.reload.email_address.address) assert_equal(false, @user.email_address.is_verified) assert_enqueued_email_with UserMailer, :email_change_confirmation, args: [@user], queue: "default" + assert_equal(true, @user.user_events.email_change.exists?) end 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(false, @user.reload.email_address.is_verified) assert_enqueued_email_with UserMailer, :email_change_confirmation, args: [@user], queue: "default" + assert_equal(true, @user.user_events.email_change.exists?) end end @@ -112,6 +114,7 @@ class EmailsControllerTest < ActionDispatch::IntegrationTest assert_response :success assert_equal("bob@ogres.net", @user.reload.email_address.address) assert_no_emails + assert_equal(false, @user.user_events.email_change.exists?) end end end diff --git a/test/functional/maintenance/user/deletions_controller_test.rb b/test/functional/maintenance/user/deletions_controller_test.rb index bf4ce204e..07fab37c3 100644 --- a/test/functional/maintenance/user/deletions_controller_test.rb +++ b/test/functional/maintenance/user/deletions_controller_test.rb @@ -18,18 +18,22 @@ module Maintenance context "#destroy" do should "delete the user when given the correct password" do delete_auth maintenance_user_deletion_path, @user, params: { user: { password: "password" }} + assert_redirected_to posts_path assert_equal(true, @user.reload.is_deleted?) assert_equal("Your account has been deactivated", flash[:notice]) assert_nil(session[:user_id]) + assert_equal(true, @user.user_events.user_deletion.exists?) end should "not delete the user when given an incorrect password" do delete_auth maintenance_user_deletion_path, @user, params: { user: { password: "hunter2" }} + assert_redirected_to maintenance_user_deletion_path assert_equal(false, @user.reload.is_deleted?) assert_equal("Password is incorrect", flash[:notice]) assert_equal(@user.id, session[:user_id]) + assert_equal(false, @user.user_events.user_deletion.exists?) end end end diff --git a/test/functional/password_resets_controller_test.rb b/test/functional/password_resets_controller_test.rb index 16c6c8588..c4ad7b123 100644 --- a/test/functional/password_resets_controller_test.rb +++ b/test/functional/password_resets_controller_test.rb @@ -16,6 +16,7 @@ class PasswordResetsControllerTest < ActionDispatch::IntegrationTest assert_redirected_to new_session_path assert_enqueued_email_with UserMailer, :password_reset, args: [@user], queue: "default" + assert_equal(true, @user.user_events.password_reset.exists?) end should "should fail if the user doesn't have a verified email address" do diff --git a/test/functional/passwords_controller_test.rb b/test/functional/passwords_controller_test.rb index f865957f2..e67822180 100644 --- a/test/functional/passwords_controller_test.rb +++ b/test/functional/passwords_controller_test.rb @@ -20,6 +20,7 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest assert_redirected_to @user assert_equal(false, @user.reload.authenticate_password("12345")) assert_equal(@user, @user.authenticate_password("abcde")) + assert_equal(true, @user.user_events.password_change.exists?) end should "update the password when given a valid login key" do @@ -29,6 +30,7 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest assert_redirected_to @user assert_equal(false, @user.reload.authenticate_password("12345")) assert_equal(@user, @user.authenticate_password("abcde")) + assert_equal(true, @user.user_events.password_change.exists?) end 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_equal(@user, @user.reload.authenticate_password("12345")) assert_equal(false, @user.authenticate_password("abcde")) + assert_equal(false, @user.user_events.password_change.exists?) end should "not update the password when password confirmation fails for the new password" do diff --git a/test/functional/sessions_controller_test.rb b/test/functional/sessions_controller_test.rb index 1eff49170..7e233b924 100644 --- a/test/functional/sessions_controller_test.rb +++ b/test/functional/sessions_controller_test.rb @@ -20,11 +20,20 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest assert_redirected_to posts_path assert_equal(@user.id, session[:user_id]) assert_not_nil(@user.reload.last_ip_addr) + assert_equal(true, @user.user_events.login.exists?) end should "not log the user in when given an incorrect password" do 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_nil(nil, session[:user_id]) end @@ -66,11 +75,18 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest end context "destroy action" do - should "clear the session" do + setup do delete_auth session_path, @user + end + + should "clear the session" do assert_redirected_to posts_path assert_nil(session[:user_id]) end + + should "generate a logout event" do + assert_equal(true, @user.user_events.logout.exists?) + end end context "sign_out action" do diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb index 0b807e4c3..e71d27c12 100644 --- a/test/functional/users_controller_test.rb +++ b/test/functional/users_controller_test.rb @@ -260,6 +260,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest assert_equal(User.last, User.last.authenticate_password("xxxxx1")) assert_nil(User.last.email_address) assert_no_enqueued_emails + assert_equal(true, User.last.user_events.user_creation.exists?) end 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("webmaster@danbooru.donmai.us", User.last.email_address.address) assert_enqueued_email_with UserMailer, :welcome_user, args: [User.last], queue: "default" + assert_equal(true, User.last.user_events.user_creation.exists?) end 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(false, User.last.is_restricted?) assert_equal(false, User.last.requires_verification) + assert_equal(true, User.last.user_events.user_creation.exists?) end 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(true, User.last.is_restricted?) assert_equal(true, User.last.requires_verification) + assert_equal(true, User.last.user_events.user_creation.exists?) end 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(true, User.last.is_restricted?) assert_equal(true, User.last.requires_verification) + assert_equal(true, User.last.user_events.user_creation.exists?) end 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(1, @ip_ban.reload.hit_count) assert(@ip_ban.last_hit_at > 1.minute.ago) + assert_equal(true, User.last.user_events.user_creation.exists?) end 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(false, User.last.is_restricted?) assert_equal(false, User.last.requires_verification) + assert_equal(true, User.last.user_events.user_creation.exists?) end 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(true, User.last.is_restricted?) assert_equal(true, User.last.requires_verification) + assert_equal(true, User.last.user_events.user_creation.exists?) end 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(false, User.last.is_restricted?) assert_equal(false, User.last.requires_verification) + assert_equal(true, User.last.user_events.user_creation.exists?) end end end diff --git a/test/unit/ip_geolocation_test.rb b/test/unit/ip_geolocation_test.rb new file mode 100644 index 000000000..b89a44e3a --- /dev/null +++ b/test/unit/ip_geolocation_test.rb @@ -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 diff --git a/test/unit/user_deletion_test.rb b/test/unit/user_deletion_test.rb index a1d8b15df..b89c45306 100644 --- a/test/unit/user_deletion_test.rb +++ b/test/unit/user_deletion_test.rb @@ -1,11 +1,18 @@ require 'test_helper' 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 "for an invalid password" do should "fail" do @user = create(:user) - @deletion = UserDeletion.new(@user, "wrongpassword") + @deletion = UserDeletion.new(@user, "wrongpassword", @request) @deletion.delete! assert_includes(@deletion.errors[:base], "Password is incorrect") end @@ -14,7 +21,7 @@ class UserDeletionTest < ActiveSupport::TestCase context "for an admin" do should "fail" do @user = create(:admin_user) - @deletion = UserDeletion.new(@user, "password") + @deletion = UserDeletion.new(@user, "password", @request) @deletion.delete! assert_includes(@deletion.errors[:base], "Admins cannot delete their account") end @@ -24,7 +31,7 @@ class UserDeletionTest < ActiveSupport::TestCase context "a valid user deletion" do setup do @user = create(:user, name: "foo", email_address: build(:email_address)) - @deletion = UserDeletion.new(@user, "password") + @deletion = UserDeletion.new(@user, "password", @request) end should "blank out the email" do