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
1
Gemfile
1
Gemfile
|
@ -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'
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
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
|
.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/
|
||||||
|
|
||||||
|
|
|
@ -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|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
# 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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
11
yarn.lock
11
yarn.lock
|
@ -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:
|
||||||
|
|
Reference in a new issue