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:
Eugen Rochko 2017-06-25 16:54:30 +02:00 committed by GitHub
parent 099a3b4eac
commit f7301bd5b9
15 changed files with 147 additions and 30 deletions

View file

@ -20,6 +20,7 @@ gem 'paperclip-av-transcoder', '~> 0.6'
gem 'addressable', '~> 2.5' gem 'addressable', '~> 2.5'
gem 'bootsnap' gem 'bootsnap'
gem 'browser'
gem 'cld3', '~> 3.1' gem 'cld3', '~> 3.1'
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
gem 'devise-two-factor', '~> 3.0' gem 'devise-two-factor', '~> 3.0'

View file

@ -70,6 +70,7 @@ GEM
bootsnap (1.0.0) bootsnap (1.0.0)
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (3.6.2) brakeman (3.6.2)
browser (2.4.0)
builder (3.2.3) builder (3.2.3)
bullet (5.5.1) bullet (5.5.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -483,6 +484,7 @@ DEPENDENCIES
binding_of_caller (~> 0.7) binding_of_caller (~> 0.7)
bootsnap bootsnap
brakeman (~> 3.6) brakeman (~> 3.6)
browser
bullet (~> 5.5) bullet (~> 5.5)
bundler-audit (~> 0.5) bundler-audit (~> 0.5)
capistrano (~> 3.8) capistrano (~> 3.8)

View file

@ -5,6 +5,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :check_enabled_registrations, only: [:new, :create] before_action :check_enabled_registrations, only: [:new, :create]
before_action :configure_sign_up_params, only: [:create] before_action :configure_sign_up_params, only: [:create]
before_action :set_sessions, only: [:edit, :update]
def destroy def destroy
not_found not_found
@ -41,4 +42,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController
def determine_layout def determine_layout
%w(edit update).include?(action_name) ? 'admin' : 'auth' %w(edit update).include?(action_name) ? 'admin' : 'auth'
end end
def set_sessions
@sessions = current_user.session_activations
end
end end

View file

@ -41,4 +41,16 @@ module SettingsHelper
def hash_to_object(hash) def hash_to_object(hash)
HashObject.new(hash) HashObject.new(hash)
end end
def session_device_icon(session)
device = session.detection.device
if device.mobile?
'mobile'
elsif device.tablet?
'tablet'
else
'desktop'
end
end
end end

View file

@ -42,6 +42,18 @@
strong { strong {
font-weight: 500; 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 { samp {

View file

@ -8,31 +8,49 @@
# session_id :string not null # session_id :string not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# user_agent :string default(""), not null
# ip :inet
# #
class SessionActivation < ApplicationRecord class SessionActivation < ApplicationRecord
LIMIT = Rails.configuration.x.max_session_activations def detection
@detection ||= Browser.new(user_agent)
def self.active?(id)
id && where(session_id: id).exists?
end end
def self.activate(id) def browser
activation = create!(session_id: id) detection.id
purge_old
activation
end end
def self.deactivate(id) def platform
return unless id detection.platform.id
where(session_id: id).destroy_all
end end
def self.purge_old before_save do
order('created_at desc').offset(LIMIT).destroy_all self.user_agent = '' if user_agent.nil?
end end
def self.exclusive(id) class << self
where('session_id != ?', id).destroy_all 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
end end

View file

@ -91,8 +91,10 @@ class User < ApplicationRecord
settings.auto_play_gif settings.auto_play_gif
end end
def activate_session def activate_session(request)
session_activations.activate(SecureRandom.hex).session_id session_activations.activate(session_id: SecureRandom.hex,
user_agent: request.user_agent,
ip: request.ip).session_id
end end
def exclusive_session(id) def exclusive_session(id)

View 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)

View file

@ -12,6 +12,10 @@
.actions .actions
= f.button :button, t('generic.save_changes'), type: :submit = f.button :button, t('generic.save_changes'), type: :submit
%hr/
= render 'sessions'
- if open_deletion? - if open_deletion?
%hr/ %hr/

View file

@ -1,6 +1,6 @@
Warden::Manager.after_set_user except: :fetch do |user, warden| Warden::Manager.after_set_user except: :fetch do |user, warden|
SessionActivation.deactivate warden.raw_session['auth_id'] 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 end
Warden::Manager.after_fetch do |user, warden| Warden::Manager.after_fetch do |user, warden|

View file

@ -320,6 +320,43 @@ en:
missing_resource: Could not find the required redirect URL for your account missing_resource: Could not find the required redirect URL for your account
proceed: Proceed to follow proceed: Proceed to follow
prompt: 'You are going 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: settings:
authorized_apps: Authorized apps authorized_apps: Authorized apps
back: Back to Mastodon back: Back to Mastodon

View file

@ -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

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -255,6 +255,8 @@ ActiveRecord::Schema.define(version: 20170623152212) do
t.string "session_id", null: false t.string "session_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_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 ["session_id"], name: "index_session_activations_on_session_id", unique: true
t.index ["user_id"], name: "index_session_activations_on_user_id" t.index ["user_id"], name: "index_session_activations_on_user_id"
end 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: "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", column: "target_account_id", on_delete: :cascade
add_foreign_key "reports", "accounts", 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", column: "in_reply_to_account_id", on_delete: :nullify
add_foreign_key "statuses", "accounts", on_delete: :cascade add_foreign_key "statuses", "accounts", on_delete: :cascade
add_foreign_key "statuses", "statuses", column: "in_reply_to_id", on_delete: :nullify add_foreign_key "statuses", "statuses", column: "in_reply_to_id", on_delete: :nullify

View file

@ -23,7 +23,7 @@ Devise::Test::ControllerHelpers.module_eval do
original_sign_in(resource, scope: scope) original_sign_in(resource, scope: scope)
SessionActivation.deactivate warden.raw_session["auth_id"] 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
end end

View file

@ -7184,16 +7184,7 @@ webpack-bundle-analyzer@^2.8.2:
opener "^1.4.3" opener "^1.4.3"
ws "^2.3.1" ws "^2.3.1"
webpack-dev-middleware@^1.10.2: webpack-dev-middleware@^1.10.2, webpack-dev-middleware@^1.11.0:
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:
version "1.11.0" version "1.11.0"
resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.11.0.tgz#09691d0973a30ad1f82ac73a12e2087f0a4754f9" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.11.0.tgz#09691d0973a30ad1f82ac73a12e2087f0a4754f9"
dependencies: dependencies: