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:
		
							parent
							
								
									099a3b4eac
								
							
						
					
					
						commit
						f7301bd5b9
					
				
					 15 changed files with 147 additions and 30 deletions
				
			
		
							
								
								
									
										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 a new issue