From f7301bd5b94d3033b5dbb9ff65dd1ed8ac825ce5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 25 Jun 2017 16:54:30 +0200 Subject: [PATCH] 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 --- Gemfile | 1 + Gemfile.lock | 2 + .../auth/registrations_controller.rb | 5 ++ app/helpers/settings_helper.rb | 12 +++++ app/javascript/styles/tables.scss | 12 +++++ app/models/session_activation.rb | 48 +++++++++++++------ app/models/user.rb | 6 ++- .../auth/registrations/_sessions.html.haml | 23 +++++++++ app/views/auth/registrations/edit.html.haml | 4 ++ config/initializers/devise.rb | 2 +- config/locales/en.yml | 37 ++++++++++++++ ..._add_description_to_session_activations.rb | 7 +++ db/schema.rb | 5 +- spec/rails_helper.rb | 2 +- yarn.lock | 11 +---- 15 files changed, 147 insertions(+), 30 deletions(-) create mode 100644 app/views/auth/registrations/_sessions.html.haml create mode 100644 db/migrate/20170624134742_add_description_to_session_activations.rb diff --git a/Gemfile b/Gemfile index 77fffe7a66..aecd82702d 100644 --- a/Gemfile +++ b/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' diff --git a/Gemfile.lock b/Gemfile.lock index 00ce84556f..627a01787b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index d385c08e13..60ace04d7b 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -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 diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 172ef33cac..847eff2e7f 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -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 diff --git a/app/javascript/styles/tables.scss b/app/javascript/styles/tables.scss index f7def8cf37..6e54c59c01 100644 --- a/app/javascript/styles/tables.scss +++ b/app/javascript/styles/tables.scss @@ -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 { diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb index 71e9f023c2..75339b5f75 100644 --- a/app/models/session_activation.rb +++ b/app/models/session_activation.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index fccf1089bb..c31a0c6440 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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) diff --git a/app/views/auth/registrations/_sessions.html.haml b/app/views/auth/registrations/_sessions.html.haml new file mode 100644 index 0000000000..11c0d4e315 --- /dev/null +++ b/app/views/auth/registrations/_sessions.html.haml @@ -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) diff --git a/app/views/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml index 38d4349cb6..fbc8d017b3 100644 --- a/app/views/auth/registrations/edit.html.haml +++ b/app/views/auth/registrations/edit.html.haml @@ -12,6 +12,10 @@ .actions = f.button :button, t('generic.save_changes'), type: :submit +%hr/ + += render 'sessions' + - if open_deletion? %hr/ diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 6d3a73ef67..d51471d308 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -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| diff --git a/config/locales/en.yml b/config/locales/en.yml index 0d33aae3f6..1d8e3f6b00 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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 diff --git a/db/migrate/20170624134742_add_description_to_session_activations.rb b/db/migrate/20170624134742_add_description_to_session_activations.rb new file mode 100644 index 0000000000..9dbb155641 --- /dev/null +++ b/db/migrate/20170624134742_add_description_to_session_activations.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index b6aceb9302..1e7d6c0b33 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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 diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 31c94b1e4b..cfc9eec9ea 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -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 diff --git a/yarn.lock b/yarn.lock index ef870d7e2a..d1a1687a0e 100644 --- a/yarn.lock +++ b/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: