diff --git a/Gemfile b/Gemfile index 77fffe7a66d8eeab10d36d91bd553dd7ea6aaebb..aecd82702d59f896063b959a6882df48d169bdb0 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 00ce84556fad25f67475bb5339eff644c3a29bb7..627a01787b2deb4e7f7a76cd11dc3f013238762f 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 d385c08e1352eecd479f9b565bd30544611abf87..60ace04d7b1e4b129389fd0499b2977b82eb23fb 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 172ef33cac63942142a3ff38e9a83f7995027276..847eff2e7f4fca35c8af091809f74950ddd7fa0a 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 f7def8cf37edf558b305513dae518e1102cf1225..6e54c59c0139baec775262b7421edc18e139beae 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 71e9f023c2d35c24b68eb75ecfd9c64faad01d40..75339b5f75c80a92d2661f7207ed0f4a482c1b82 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 fccf1089bb7cc0e429685f88d0fb6982dc0ba30f..c31a0c644049b56840be675689781211fbc47d00 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 0000000000000000000000000000000000000000..11c0d4e315f998272eabd1e9c9744e4a8815a276 --- /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 38d4349cb6a841d98f8be35363ab7ad5fd0e4af5..fbc8d017b36fd1bc7359a4f1e67f87c209fe277f 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 6d3a73ef670b4edb6bf6e5b002a17f36c54974f7..d51471d308ad38230c035a130ccfb2f63281aa5a 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 0d33aae3f684bd0a0c98f3f3cd140b60ac681b4c..1d8e3f6b00c394a199123ae9122db8f65045dbe3 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 0000000000000000000000000000000000000000..9dbb155641151562ceefe8e3eaa06e5fb131df24 --- /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 b6aceb930278e83711fe671d7a2be74a2f704340..1e7d6c0b33710193427f3b44361a51c8bc01077e 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 31c94b1e4b99499d194516ef2ea5aec864e2022a..cfc9eec9ea0c37e5d5ed0398b715512af133f801 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 ef870d7e2add66d94b53961779301d93a895b92a..d1a1687a0e65b756c6ace52f2edf54f99ac73302 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: