diff --git a/app/assets/javascripts/session_end.js b/app/assets/javascripts/session_end.js new file mode 100644 index 000000000..5f04bb78e --- /dev/null +++ b/app/assets/javascripts/session_end.js @@ -0,0 +1,68 @@ +/* globals I18n */ + +(function() { + 'use strict'; + + var originalTitle = ''; + var expireIn; + var expireLimit = 900; // 15min + var timeoutID; + var expirationUrl = $('meta[name=\'expiration-url\']').attr('content'); + + var pad = function(i) { + var s = ('0' + Math.floor(i)); + return s.substring(s.length - 2); + }; + + var newTimerStr = function(expirationTime) { + var m = (expirationTime / 60) % 60; + var s = (expirationTime % 60); + return [m, s].map(pad).join(':'); + }; + + function getSessionEnd() { + if (expirationUrl) { + $.get(expirationUrl, function(data) { + if (data <= 0) { + $('#session-finished').modal(); + } else if (data <= expireLimit + 1) { + expireIn = data; + originalTitle = document.title; + // eslint-disable-next-line no-use-before-define + timeoutID = setTimeout(expirationInTime, 1000); + } else { + timeoutID = setTimeout(getSessionEnd, (data - expireLimit) * 1000); + } + }); + } + } + + function expirationInTime() { + if (expireIn > 0) { + document.title = newTimerStr(expireIn) + ' ' + originalTitle; + $('.expiring').text(I18n.t('devise.sessions.expire_modal.session_end_in.header', + { time: newTimerStr(expireIn) })); + expireIn -= 1; + if (!$('#session-expire').hasClass('in')) { + $('#session-expire').modal().off('hide.bs.modal').on('hide.bs.modal', function() { + if (expireIn > 0) { + $.post($('meta[name=\'revive-url\']').attr('content')); + document.title = originalTitle; + clearTimeout(timeoutID); + timeoutID = setTimeout(getSessionEnd, 1000); + } + }); + } + timeoutID = setTimeout(expirationInTime, 1000); + } else { + document.title = originalTitle; + $('#session-expire').modal('hide'); + $('#session-finished').modal(); + } + } + timeoutID = setTimeout(getSessionEnd, 1000); + + $(document).on('click', '.session-login', function() { + window.location.reload(); + }); +}()); diff --git a/app/assets/stylesheets/session_expired.scss b/app/assets/stylesheets/session_expired.scss new file mode 100644 index 000000000..adb9df039 --- /dev/null +++ b/app/assets/stylesheets/session_expired.scss @@ -0,0 +1,39 @@ + +// scss-lint:disable NestingDepth ImportantRule + +.session-modal { + .modal-body { + padding-bottom: 0px; + } + + ul { + list-style-type: none; + padding-left: 0; + + li { + padding: .25em 0; + } + } + + a:hover { + text-decoration: none; + } + + .instruction-session-collapse { + cursor: pointer; + margin-bottom: .5em; + margin-top: 20px; + + .fa-angle-up { + margin-left: .5em; + } + + &.collapsed { + + .fa-angle-up { + @include rotate(-180deg); + } + } + } + +} diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 97a66cd44..d8cab9149 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -4,6 +4,7 @@ class Users::SessionsController < Devise::SessionsController layout :session_layout after_action :after_sign_in, only: %i(create authenticate_with_two_factor) before_action :remove_authenticate_mesasge_if_root_path, only: :new + prepend_before_action :skip_timeout, only: :expire_in rescue_from ActionController::InvalidAuthenticityToken do redirect_to new_user_session_path @@ -34,6 +35,16 @@ class Users::SessionsController < Devise::SessionsController generate_templates_project end + def expire_in + if current_user.remember_created_at.nil? + render plain: Devise.timeout_in.to_i - (Time.now.to_i - user_session['last_request_at']).round + else + render plain: Devise.remember_for - (Time.now.to_i - current_user.remember_created_at.to_i).round + end + end + + def revive_session; end + def two_factor_recovery unless session[:otp_user_id] redirect_to new_user_session_path @@ -92,6 +103,10 @@ class Users::SessionsController < Devise::SessionsController private + def skip_timeout + request.env['devise.skip_trackable'] = true + end + def remove_authenticate_mesasge_if_root_path if session[:user_return_to] == root_path && flash[:alert] == I18n.t('devise.failure.unauthenticated') flash[:alert] = nil diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 367e75e27..004cfb215 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -7,9 +7,14 @@ + <% if user_signed_in? %> + + + <% end %> <%= stylesheet_link_tag 'application', media: 'all' %> <%= javascript_include_tag 'application' %> <%= javascript_pack_tag 'application' %> + <%= javascript_include_tag 'session_end' %> @@ -57,6 +62,8 @@ <%= render "shared/file_edit_modal.html.erb" %> <%= render "shared/marvinjs_modal.html.erb" %> <%= render "shared/comments/comments_sidebar.html.erb" %> + <%= render "users/sessions/session_expire_modal.html.erb" %> + <%= render "users/sessions/session_end_modal.html.erb" %> <% end %> <% if user_signed_in? && flash[:system_notification_modal] && current_user.show_login_system_notification? %> diff --git a/app/views/users/sessions/_session_end_modal.html.erb b/app/views/users/sessions/_session_end_modal.html.erb new file mode 100644 index 000000000..d35a1420b --- /dev/null +++ b/app/views/users/sessions/_session_end_modal.html.erb @@ -0,0 +1,39 @@ + diff --git a/app/views/users/sessions/_session_expire_modal.html.erb b/app/views/users/sessions/_session_expire_modal.html.erb new file mode 100644 index 000000000..48ea54f35 --- /dev/null +++ b/app/views/users/sessions/_session_expire_modal.html.erb @@ -0,0 +1,34 @@ + diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index e85038740..48afa6362 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -91,6 +91,7 @@ Rails.application.config.assets.precompile += %w(sidebar_toggle.js) Rails.application.config.assets.precompile += %w(reports/reports_datatable.js) Rails.application.config.assets.precompile += %w(reports/save_pdf_to_inventory.js) Rails.application.config.assets.precompile += %w(reports/content.js) +Rails.application.config.assets.precompile += %w(session_end.js) # Libraries needed for Handsontable formulas Rails.application.config.assets.precompile += %w(jquery.js) diff --git a/config/locales/en.yml b/config/locales/en.yml index 4ed25b874..3882352d7 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -49,6 +49,17 @@ en: not_correct_code: "Not correct recovery code" create: team_name: "%{user}'s projects" + expire_modal: + why_see_this: 'Why am I seeing this?' + session_end_in: + header: 'Your session will end in %{time}' + description: 'To keep the data you entered into the fields without clicking the Save button, please revive your session and save them.' + paragraph1: 'To keep your work secure, SciNote times out sessions after 3 hours of inactivity. ' + paragraph2: 'To proceed working after the session ends, you’ll have to log in again, starting a new working session.' + revive_session: 'Revive my session' + session_ended: + header: 'To proceed working, please log in again' + description: 'If you have any unsaved data, please close this message, copy your text, and paste it back after you log in again.' unlocks: new: head_title: "Resend unlock instructions" diff --git a/config/routes.rb b/config/routes.rb index e7c808ff0..a538aff47 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -703,6 +703,8 @@ Rails.application.routes.draw do get 'users/sign_up_provider' => 'users/registrations#new_with_provider' get 'users/two_factor_recovery' => 'users/sessions#two_factor_recovery' get 'users/two_factor_auth' => 'users/sessions#two_factor_auth' + get 'users/expire_in' => 'users/sessions#expire_in' + post 'users/revive_session' => 'users/sessions#revive_session' post 'users/authenticate_with_two_factor' => 'users/sessions#authenticate_with_two_factor' post 'users/authenticate_with_recovery_code' => 'users/sessions#authenticate_with_recovery_code' post 'users/complete_sign_up_provider' => 'users/registrations#create_with_provider'