Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to easily extend Auth#store_location behavior #3369

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions core/app/models/spree/user_last_url_storer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

module Spree
# This service object is responsible for storing the current path into
# into `session[:spree_user_return_to]` for redirects after successful
# user/admin authentication.
class UserLastUrlStorer
# Lists all the rules that will be evaluated before storing the
# current path value into the session.
#
# @return [Spree::Core::ClassConstantizer::Set] a set of rules
# that, when matched, will prevent session[:spree_user_return_to]
# to be set
#
# @example This method can be used also to add more rules
# Spree::UserLastUrlStorer.rules << 'CustomRule'
#
# @example it can be used also for removing unwanted rules
# Spree::UserLastUrlStorer.rules.delete('CustomRule')
#
def self.rules
Spree::Config.user_last_url_storer_rules
end

# @param controller [ApplicationController] an instance of ApplicationController
# or its subclasses. The controller will be passed to each rule for matching.
def initialize(controller)
@controller = controller
end

# Stores into session[:spree_user_return_to] the request full path for
# future redirects (to be used after successful authentication). When
# there is a rule match then the request full path is not stored.
def store_location
return if self.class.rules.any? { |rule| rule.match? controller }

session[:spree_user_return_to] = request.fullpath.gsub('//', '/')
end

private

attr_reader :controller

delegate :session, :request, to: :controller
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

module Spree
class UserLastUrlStorer
module Rules
# This is the basic rule that ships with Solidus that avoids storing in
# session the current path for login/loout/signup routes, avoiding possibly
# infinte redirects.
module AuthenticationRule
AUTHENTICATION_ROUTES = %w[spree_signup_path spree_login_path spree_logout_path]

extend self

def match?(controller)
full_path = controller.request.fullpath
disallowed_urls(controller).include?(full_path)
end

private

def disallowed_urls(controller)
@disallowed_urls ||= {}
@disallowed_urls[controller.controller_name] ||= begin
[].tap do |disallowed_urls|
AUTHENTICATION_ROUTES.each do |route|
if controller.respond_to?(route)
disallowed_urls << controller.send(route)
end
end
end.map! { |url| url[/\/\w+$/] }
end
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,11 @@ Spree.user_class = <%= (options[:user_class].blank? ? "Spree::LegacyUser" : opti
# just uncomment the following code and change it as you need.
#
# Spree::Model.whitelisted_ransackable_attributes << 'field'

# Rules for avoiding to store the current path into session for redirects
# When at least one rule is matched, the request path will not be stored
# in session.
# You can add your custom rules by uncommenting this line and changing
# the class name:
#
# Spree::UserLastUrlStorer.rules << 'Spree::UserLastUrlStorer::Rules::AuthenticationRule'
6 changes: 6 additions & 0 deletions core/lib/spree/app_configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,12 @@ def events
@events_configuration ||= Spree::Event::Configuration.new
end

def user_last_url_storer_rules
@user_last_url_storer_rules ||= ::Spree::Core::ClassConstantizer::Set.new.tap do |set|
set << 'Spree::UserLastUrlStorer::Rules::AuthenticationRule'
end
end

def environment
@environment ||= Spree::Core::Environment.new(self).tap do |env|
env.calculators.promotion_actions_create_adjustments = %w[
Expand Down
14 changes: 1 addition & 13 deletions core/lib/spree/core/controller_helpers/auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,7 @@ def set_guest_token
end

def store_location
# disallow return to login, logout, signup pages
authentication_routes = [:spree_signup_path, :spree_login_path, :spree_logout_path]
disallowed_urls = []
authentication_routes.each do |route|
if respond_to?(route)
disallowed_urls << send(route)
end
end

disallowed_urls.map!{ |url| url[/\/\w+$/] }
unless disallowed_urls.include?(request.fullpath)
session['spree_user_return_to'] = request.fullpath.gsub('//', '/')
end
Spree::UserLastUrlStorer.new(self).store_location
end

# proxy method to *possible* spree_current_user method
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Spree::UserLastUrlStorer::Rules::AuthenticationRule do
describe '#match?' do
let(:login_path) { '/sign_in' }
let(:request) { double(fullpath: fullpath) }
let(:controller) do
double(
request: request,
spree_login_path: login_path,
controller_name: 'controller_double'
)
end

subject { described_class.match?(controller) }

context 'when the request full path is an authentication route' do
let!(:fullpath) { login_path }

it { is_expected.to be true }
end

context 'when the request full path is not an authentication route' do
let!(:fullpath) { '/products/baseball-cap' }

it { is_expected.to be false }
end
end
end
60 changes: 60 additions & 0 deletions core/spec/models/spree/user_last_url_storer_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Spree::UserLastUrlStorer do
subject { described_class.new(controller) }

let(:fullpath) { '/products/baseball-cap' }
let(:session) { {} }
let(:request) { double(fullpath: fullpath) }
let(:controller) do
instance_double(
ApplicationController,
request: request,
session: session,
controller_name: 'app_controller_double'
)
end

module CustomRule
def self.match?(_controller)
true
end
end

after :each do
described_class.rules.delete('CustomRule')
end

describe '::rules' do
it 'includes default rules' do
rule = Spree::UserLastUrlStorer::Rules::AuthenticationRule
expect(described_class.rules).to include(rule)
end

it 'can add new rules' do
described_class.rules << CustomRule
expect(described_class.rules).to include(CustomRule)
end
end

describe '#store_location' do
context 'when at least one rule matches' do
it 'does not set the path value into the session' do
described_class.rules << CustomRule
subject.store_location
expect(session[:spree_user_return_to]).to be_nil
end
end

context 'when no rule matches' do
it 'sets the path value into the session' do
described_class.rules << CustomRule
described_class.rules.delete('CustomRule')
subject.store_location
expect(session[:spree_user_return_to]).to eql fullpath
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Customizing After-login Redirects

Standard Solidus installations use the `solidus_auth_devise` gem
in order to provide user authentication. The gem is based on
`Devise`, a very successful authentication gem for Rails.

When the unauthenticated user visits an authentication-protected page, they're
first redirected to the login page, eventually after successful login they're
redirected back to the page they were originally wanting to visit.

Before redirecting the user to the login page, Solidus stores the original URL
that the user wanted to visit into Rails application session cookie, ie.
`session[:spree_return_to]`.

There are some URLs that we need to avoid storing in session, othwewise
inifite-loops would occur after successful authentication.

All of these URLs with a standard Solidus installation are related to the
authentication process, but you may need to add more, for example because you
added some more authentication URLs.

Solidus uses rules managed by the service object [`Spree::UserLastUrlStorer`][user-last-url-storer]
in order to decide whether the current path should be stored or not. The
default rule is defined in [`Spree::UserLastUrlStorer::Rules::AuthenticationRule`][auth-rule].

In order to add your custom behavior, you can create a new rule:

```ruby
module Spree
class UserLastUrlStorer
module Rules
module FacebookLoginRule
extend self

def match?(controller)
controller.controller_name == "sessions" &&
action_name == "facebook_login"
end
end
end
end
end
```

After that, you need to register your new rule module, for example by adding
this line in `config/spree.rb` file:

```ruby
Spree::UserLastUrlStorer.rules << 'Spree::UserLastUrlStorer::Rules::FacebookLoginRule'
```

Please note that, when at least one rule is met (`#match?` returns `true`) then
the current path **is not** stored in the session.

[user-last-url-storer]: https://github.com/solidusio/solidus/blob/master/core/app/models/spree/user_last_url_storer.rb
[auth-rule]: https://github.com/solidusio/solidus/blob/master/core/app/models/spree/user_last_url_storer/rules/authentication_rule.rb
6 changes: 4 additions & 2 deletions guides/source/developers/customizations/overview.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Solidus is very flexible. It allows you to customize every part of the store, bo
the customer-facing storefront part (also called "frontend") and the admin panel
(also called "backend").

Some customizations are very easy to implement, even for inexperienced developers.
Others may require a solid understanding of the Ruby and Ruby on Rails ecosystem,
Some customizations are very easy to implement, even for inexperienced developers.
Others may require a solid understanding of the Ruby and Ruby on Rails ecosystem,
which are the language and the framework that power Solidus, respectively. This
guide will provide a brief introduction to the different types of customization
that are possible using Solidus.
Expand All @@ -19,6 +19,7 @@ that are possible using Solidus.
- [Learn How to Customize Permissions][permissions]
- [Learn How to Customize Attributes][attributes]
- [Learn How to Customize Mailers][mailers]
- [Learn How to Customize After-login Redirects][after-login-redirects]

[storefront]: customizing-storefront.html
[admin]: customizing-admin.html
Expand All @@ -27,3 +28,4 @@ that are possible using Solidus.
[permissions]: customizing-permissions.html
[attributes]: customizing-attributes.html
[mailers]: customizing-mailers.html
[after-login-redirects]: customizing-after-login-redirects.html