diff --git a/app/models/api_token.rb b/app/models/api_token.rb index f2e9e00180..46b6b64952 100644 --- a/app/models/api_token.rb +++ b/app/models/api_token.rb @@ -6,6 +6,7 @@ # # id :bigint not null, primary key # expires_in :integer +# ip_address :inet # refresh_token :string # revoked_at :datetime # scopes :string @@ -19,6 +20,7 @@ # Indexes # # index_api_tokens_on_application_id (application_id) +# index_api_tokens_on_ip_address (ip_address) # index_api_tokens_on_token_bidx (token_bidx) UNIQUE # index_api_tokens_on_user_id (user_id) # @@ -53,4 +55,19 @@ def self.generate(options = {}) def abbreviated = "#{token[..7]}...#{token[-3..]}" + def geocode_result + return nil unless ip_address.present? + return @geocode_result if defined?(@geocode_result) + + @geocode_result = Geocoder.search(ip_address.to_s)&.first + end + + def latitude + geocode_result&.latitude + end + + def longitude + geocode_result&.longitude + end + end diff --git a/app/views/users/_oauth_authorization.erb b/app/views/users/_oauth_authorization.erb index 84963cee36..952476b805 100644 --- a/app/views/users/_oauth_authorization.erb +++ b/app/views/users/_oauth_authorization.erb @@ -49,6 +49,20 @@ <% end %> + <% latest_token_with_ip = authorization.tokens.where.not(ip_address: nil).order(created_at: :desc).first %> + <% if latest_token_with_ip.present? %> + <% if latest_token_with_ip.latitude.present? && latest_token_with_ip.longitude.present? %> + + <% else %> +
+ Location unavailable +
+ <% end %> + <% if latest_token_with_ip.ip_address.present? %> +
+ <%= inline_icon "web", size: 20 %> <%= latest_token_with_ip.ip_address %> +
+ <% end %>

<%= authorization.authorization_count == 1 ? "Authorized" : "Last authorized" %> <%= local_time_ago authorization.created_at %>

diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 2e5be4504d..d3fa9bceb0 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -357,7 +357,7 @@ # tokens, you can check that the requested data belongs to the specified tenant. # # Default value is an empty Array: [] - # custom_access_token_attributes [:tenant_id] + custom_access_token_attributes [:ip_address] # Hook into the strategies' request & response life-cycle in case your # application needs advanced customization or logging: @@ -366,9 +366,12 @@ # puts "BEFORE HOOK FIRED! #{request}" # end # - # after_successful_strategy_response do |request, response| - # puts "AFTER HOOK FIRED! #{request}, #{response}" - # end + after_successful_strategy_response do |request, response| + if response.respond_to?(:token) && response.token.is_a?(ApiToken) + ip = request.remote_ip + response.token.update(ip_address: ip) if ip.present? + end + end # Hook into Authorization flow in order to implement Single Sign Out # or add any other functionality. Inside the block you have an access diff --git a/db/migrate/20251117030308_add_ip_address_to_api_tokens.rb b/db/migrate/20251117030308_add_ip_address_to_api_tokens.rb new file mode 100644 index 0000000000..fedaa9b00e --- /dev/null +++ b/db/migrate/20251117030308_add_ip_address_to_api_tokens.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class AddIpAddressToApiTokens < ActiveRecord::Migration[8.0] + disable_ddl_transaction! + + def change + safety_assured do + add_column :api_tokens, :ip_address, :inet + add_index :api_tokens, :ip_address, algorithm: :concurrently + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 807d8c3be9..689bd83668 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -12,7 +12,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_11_15_104532) do +ActiveRecord::Schema[8.0].define(version: 2025_11_17_030308) do create_schema "google_sheets" # These are extensions that must be enabled in order to support this database @@ -227,7 +227,9 @@ t.string "refresh_token" t.integer "expires_in" t.string "scopes" + t.inet "ip_address" t.index ["application_id"], name: "index_api_tokens_on_application_id" + t.index ["ip_address"], name: "index_api_tokens_on_ip_address" t.index ["token_bidx"], name: "index_api_tokens_on_token_bidx", unique: true t.index ["user_id"], name: "index_api_tokens_on_user_id" end