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