Add overview of active sessions (#3929)
* Add overview of active sessions * Better display of browser/platform name * Improve how browser information is stored and displayed for sessions overview * Fix test
This commit is contained in:
		
							
								
								
									
										1
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								Gemfile
									
									
									
									
									
								
							| @@ -20,6 +20,7 @@ gem 'paperclip-av-transcoder', '~> 0.6' | ||||
|  | ||||
| gem 'addressable', '~> 2.5' | ||||
| gem 'bootsnap' | ||||
| gem 'browser' | ||||
| gem 'cld3', '~> 3.1' | ||||
| gem 'devise', '~> 4.2' | ||||
| gem 'devise-two-factor', '~> 3.0' | ||||
|   | ||||
| @@ -70,6 +70,7 @@ GEM | ||||
|     bootsnap (1.0.0) | ||||
|       msgpack (~> 1.0) | ||||
|     brakeman (3.6.2) | ||||
|     browser (2.4.0) | ||||
|     builder (3.2.3) | ||||
|     bullet (5.5.1) | ||||
|       activesupport (>= 3.0.0) | ||||
| @@ -483,6 +484,7 @@ DEPENDENCIES | ||||
|   binding_of_caller (~> 0.7) | ||||
|   bootsnap | ||||
|   brakeman (~> 3.6) | ||||
|   browser | ||||
|   bullet (~> 5.5) | ||||
|   bundler-audit (~> 0.5) | ||||
|   capistrano (~> 3.8) | ||||
|   | ||||
| @@ -5,6 +5,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController | ||||
|  | ||||
|   before_action :check_enabled_registrations, only: [:new, :create] | ||||
|   before_action :configure_sign_up_params, only: [:create] | ||||
|   before_action :set_sessions, only: [:edit, :update] | ||||
|  | ||||
|   def destroy | ||||
|     not_found | ||||
| @@ -41,4 +42,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController | ||||
|   def determine_layout | ||||
|     %w(edit update).include?(action_name) ? 'admin' : 'auth' | ||||
|   end | ||||
|  | ||||
|   def set_sessions | ||||
|     @sessions = current_user.session_activations | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -41,4 +41,16 @@ module SettingsHelper | ||||
|   def hash_to_object(hash) | ||||
|     HashObject.new(hash) | ||||
|   end | ||||
|  | ||||
|   def session_device_icon(session) | ||||
|     device = session.detection.device | ||||
|  | ||||
|     if device.mobile? | ||||
|       'mobile' | ||||
|     elsif device.tablet? | ||||
|       'tablet' | ||||
|     else | ||||
|       'desktop' | ||||
|     end | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -42,6 +42,18 @@ | ||||
|   strong { | ||||
|     font-weight: 500; | ||||
|   } | ||||
|  | ||||
|   &.inline-table { | ||||
|     td, | ||||
|     th { | ||||
|       padding: 8px 0; | ||||
|     } | ||||
|  | ||||
|     & > tbody > tr:nth-child(odd) > td, | ||||
|     & > tbody > tr:nth-child(odd) > th { | ||||
|       background: transparent; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| samp { | ||||
|   | ||||
| @@ -8,31 +8,49 @@ | ||||
| #  session_id :string           not null | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
| #  user_agent :string           default(""), not null | ||||
| #  ip         :inet | ||||
| # | ||||
|  | ||||
| class SessionActivation < ApplicationRecord | ||||
|   LIMIT = Rails.configuration.x.max_session_activations | ||||
|  | ||||
|   def self.active?(id) | ||||
|     id && where(session_id: id).exists? | ||||
|   def detection | ||||
|     @detection ||= Browser.new(user_agent) | ||||
|   end | ||||
|  | ||||
|   def self.activate(id) | ||||
|     activation = create!(session_id: id) | ||||
|     purge_old | ||||
|     activation | ||||
|   def browser | ||||
|     detection.id | ||||
|   end | ||||
|  | ||||
|   def self.deactivate(id) | ||||
|     return unless id | ||||
|     where(session_id: id).destroy_all | ||||
|   def platform | ||||
|     detection.platform.id | ||||
|   end | ||||
|  | ||||
|   def self.purge_old | ||||
|     order('created_at desc').offset(LIMIT).destroy_all | ||||
|   before_save do | ||||
|     self.user_agent = '' if user_agent.nil? | ||||
|   end | ||||
|  | ||||
|   def self.exclusive(id) | ||||
|     where('session_id != ?', id).destroy_all | ||||
|   class << self | ||||
|     def active?(id) | ||||
|       id && where(session_id: id).exists? | ||||
|     end | ||||
|  | ||||
|     def activate(options = {}) | ||||
|       activation = create!(options) | ||||
|       purge_old | ||||
|       activation | ||||
|     end | ||||
|  | ||||
|     def deactivate(id) | ||||
|       return unless id | ||||
|       where(session_id: id).destroy_all | ||||
|     end | ||||
|  | ||||
|     def purge_old | ||||
|       order('created_at desc').offset(Rails.configuration.x.max_session_activations).destroy_all | ||||
|     end | ||||
|  | ||||
|     def exclusive(id) | ||||
|       where('session_id != ?', id).destroy_all | ||||
|     end | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -91,8 +91,10 @@ class User < ApplicationRecord | ||||
|     settings.auto_play_gif | ||||
|   end | ||||
|  | ||||
|   def activate_session | ||||
|     session_activations.activate(SecureRandom.hex).session_id | ||||
|   def activate_session(request) | ||||
|     session_activations.activate(session_id: SecureRandom.hex, | ||||
|                                  user_agent: request.user_agent, | ||||
|                                  ip: request.ip).session_id | ||||
|   end | ||||
|  | ||||
|   def exclusive_session(id) | ||||
|   | ||||
							
								
								
									
										23
									
								
								app/views/auth/registrations/_sessions.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								app/views/auth/registrations/_sessions.html.haml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| %h6= t 'sessions.title' | ||||
| %p.muted-hint= t 'sessions.explanation' | ||||
|  | ||||
| %table.table.inline-table | ||||
|   %thead | ||||
|     %tr | ||||
|       %th= t 'sessions.browser' | ||||
|       %th= t 'sessions.ip' | ||||
|       %th= t 'sessions.activity' | ||||
|   %tbody | ||||
|     - @sessions.each do |session| | ||||
|       %tr | ||||
|         %td | ||||
|           %span{ title: session.user_agent }= fa_icon session_device_icon(session) | ||||
|           = ' ' | ||||
|           = t 'sessions.description', browser: t("sessions.browsers.#{session.browser}"), platform: t("sessions.platforms.#{session.platform}") | ||||
|         %td | ||||
|           %samp= session.ip | ||||
|         %td | ||||
|           - if request.session['auth_id'] == session.session_id | ||||
|             = t 'sessions.current_session' | ||||
|           - else | ||||
|             %time.time-ago{ datetime: session.updated_at.iso8601, title: l(session.updated_at) }= l(session.updated_at) | ||||
| @@ -12,6 +12,10 @@ | ||||
|   .actions | ||||
|     = f.button :button, t('generic.save_changes'), type: :submit | ||||
|  | ||||
| %hr/ | ||||
|  | ||||
| = render 'sessions' | ||||
|  | ||||
| - if open_deletion? | ||||
|   %hr/ | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| Warden::Manager.after_set_user except: :fetch do |user, warden| | ||||
|   SessionActivation.deactivate warden.raw_session['auth_id'] | ||||
|   warden.raw_session['auth_id'] = user.activate_session | ||||
|   warden.raw_session['auth_id'] = user.activate_session(warden.request) | ||||
| end | ||||
|  | ||||
| Warden::Manager.after_fetch do |user, warden| | ||||
|   | ||||
| @@ -320,6 +320,43 @@ en: | ||||
|     missing_resource: Could not find the required redirect URL for your account | ||||
|     proceed: Proceed to follow | ||||
|     prompt: 'You are going to follow:' | ||||
|   sessions: | ||||
|     activity: Last activity | ||||
|     browser: Browser | ||||
|     browsers: | ||||
|       alipay: Alipay | ||||
|       blackberry: Blackberry | ||||
|       chrome: Chrome | ||||
|       edge: Microsoft Edge | ||||
|       firefox: Firefox | ||||
|       generic: Unknown browser | ||||
|       ie: Internet Explorer | ||||
|       micro_messenger: MicroMessenger | ||||
|       nokia: Nokia S40 Ovi Browser | ||||
|       opera: Opera | ||||
|       phantom_js: PhantomJS | ||||
|       qq: QQ Browser | ||||
|       safari: Safari | ||||
|       uc_browser: UCBrowser | ||||
|       weibo: Weibo | ||||
|     current_session: Current session | ||||
|     description: "%{browser} on %{platform}" | ||||
|     explanation: These are the web browsers currently logged in to your Mastodon account. | ||||
|     ip: IP | ||||
|     platforms: | ||||
|       adobe_air: Adobe Air | ||||
|       android: Android | ||||
|       blackberry: Blackberry | ||||
|       chrome_os: ChromeOS | ||||
|       firefox_os: Firefox OS | ||||
|       ios: iOS | ||||
|       linux: Linux | ||||
|       mac: Mac | ||||
|       other: unknown platform | ||||
|       windows: Windows | ||||
|       windows_mobile: Windows Mobile | ||||
|       windows_phone: Windows Phone | ||||
|     title: Sessions | ||||
|   settings: | ||||
|     authorized_apps: Authorized apps | ||||
|     back: Back to Mastodon | ||||
|   | ||||
| @@ -0,0 +1,7 @@ | ||||
| class AddDescriptionToSessionActivations < ActiveRecord::Migration[5.1] | ||||
|   def change | ||||
|     add_column :session_activations, :user_agent, :string, null: false, default: '' | ||||
|     add_column :session_activations, :ip, :inet | ||||
|     add_foreign_key :session_activations, :users, on_delete: :cascade | ||||
|   end | ||||
| end | ||||
| @@ -10,7 +10,7 @@ | ||||
| # | ||||
| # It's strongly recommended that you check this file into your version control system. | ||||
|  | ||||
| ActiveRecord::Schema.define(version: 20170623152212) do | ||||
| ActiveRecord::Schema.define(version: 20170624134742) do | ||||
|  | ||||
|   # These are extensions that must be enabled in order to support this database | ||||
|   enable_extension "plpgsql" | ||||
| @@ -255,6 +255,8 @@ ActiveRecord::Schema.define(version: 20170623152212) do | ||||
|     t.string "session_id", null: false | ||||
|     t.datetime "created_at", null: false | ||||
|     t.datetime "updated_at", null: false | ||||
|     t.string "user_agent", default: "", null: false | ||||
|     t.inet "ip" | ||||
|     t.index ["session_id"], name: "index_session_activations_on_session_id", unique: true | ||||
|     t.index ["user_id"], name: "index_session_activations_on_user_id" | ||||
|   end | ||||
| @@ -404,6 +406,7 @@ ActiveRecord::Schema.define(version: 20170623152212) do | ||||
|   add_foreign_key "reports", "accounts", column: "action_taken_by_account_id", on_delete: :nullify | ||||
|   add_foreign_key "reports", "accounts", column: "target_account_id", on_delete: :cascade | ||||
|   add_foreign_key "reports", "accounts", on_delete: :cascade | ||||
|   add_foreign_key "session_activations", "users", on_delete: :cascade | ||||
|   add_foreign_key "statuses", "accounts", column: "in_reply_to_account_id", on_delete: :nullify | ||||
|   add_foreign_key "statuses", "accounts", on_delete: :cascade | ||||
|   add_foreign_key "statuses", "statuses", column: "in_reply_to_id", on_delete: :nullify | ||||
|   | ||||
| @@ -23,7 +23,7 @@ Devise::Test::ControllerHelpers.module_eval do | ||||
|     original_sign_in(resource, scope: scope) | ||||
|  | ||||
|     SessionActivation.deactivate warden.raw_session["auth_id"] | ||||
|     warden.raw_session["auth_id"] = resource.activate_session | ||||
|     warden.raw_session["auth_id"] = resource.activate_session(warden.request) | ||||
|   end | ||||
| end | ||||
|  | ||||
|   | ||||
							
								
								
									
										11
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								yarn.lock
									
									
									
									
									
								
							| @@ -7184,16 +7184,7 @@ webpack-bundle-analyzer@^2.8.2: | ||||
|     opener "^1.4.3" | ||||
|     ws "^2.3.1" | ||||
|  | ||||
| webpack-dev-middleware@^1.10.2: | ||||
|   version "1.10.2" | ||||
|   resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.10.2.tgz#2e252ce1dfb020dbda1ccb37df26f30ab014dbd1" | ||||
|   dependencies: | ||||
|     memory-fs "~0.4.1" | ||||
|     mime "^1.3.4" | ||||
|     path-is-absolute "^1.0.0" | ||||
|     range-parser "^1.0.3" | ||||
|  | ||||
| webpack-dev-middleware@^1.11.0: | ||||
| webpack-dev-middleware@^1.10.2, webpack-dev-middleware@^1.11.0: | ||||
|   version "1.11.0" | ||||
|   resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.11.0.tgz#09691d0973a30ad1f82ac73a12e2087f0a4754f9" | ||||
|   dependencies: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user