diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 58909603..089c60c1 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -294,12 +294,13 @@ RSpec/StubbedMock: Exclude: - 'spec/models/user_spec.rb' -# Offense count: 2 +# Offense count: 3 # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: Exclude: - 'spec/features/confirmation_spec.rb' - 'spec/models/user_spec.rb' + - 'spec/controllers/spree/admin/user_unlocks_controller_spec.rb' # Offense count: 12 # This cop supports unsafe autocorrection (--autocorrect-all). diff --git a/app/mailers/spree/user_mailer.rb b/app/mailers/spree/user_mailer.rb index 2ee9f3b3..94c68c2c 100644 --- a/app/mailers/spree/user_mailer.rb +++ b/app/mailers/spree/user_mailer.rb @@ -13,5 +13,13 @@ def confirmation_instructions(user, token, _opts = {}) @confirmation_url = spree.spree_user_confirmation_url(confirmation_token: token, host: @store.url) mail to: user.email, from: from_address(@store), subject: "#{@store.name} #{I18n.t(:subject, scope: [:devise, :mailer, :confirmation_instructions])}" end + + def unlock_instructions(user, token, _opts = {}) + @store = Spree::Store.default + @user = user + + @unlock_url = spree.admin_unlock_url(unlock_token: token, host: @store.url) + mail to: user.email, from: from_address(@store), subject: "#{@store.name} #{I18n.t(:subject, scope: [:devise, :mailer, :unlock_instructions])}" + end end end diff --git a/app/models/spree/user.rb b/app/models/spree/user.rb index ab0fd927..031a468c 100644 --- a/app/models/spree/user.rb +++ b/app/models/spree/user.rb @@ -5,7 +5,8 @@ class User < Spree::Base include UserMethods devise :database_authenticatable, :registerable, :recoverable, - :rememberable, :trackable, :validatable, :encryptable + :rememberable, :trackable, :validatable, :encryptable, + :lockable devise :confirmable if Spree::Auth::Config[:confirmable] if defined?(Spree::SoftDeletable) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 5f724ef1..7de243ad 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -81,14 +81,14 @@ # Defines which strategy will be used to lock an account. # :failed_attempts = Locks an account after a number of failed attempts to sign in. # :none = No lock strategy. You should handle locking by yourself. - # config.lock_strategy = :failed_attempts + config.lock_strategy = :none # Defines which strategy will be used to unlock an account. # :email = Sends an unlock link to the user email # :time = Re-enables login after a certain amount of time (see :unlock_in below) # :both = Enables both strategies # :none = No unlock strategy. You should handle unlocking by yourself. - # config.unlock_strategy = :both + config.unlock_strategy = :both # Number of authentication tries before locking an account if lock_strategy # is failed attempts. diff --git a/config/routes.rb b/config/routes.rb index 39613b7d..5d78e6c5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -46,7 +46,8 @@ path_names: { sign_out: 'logout' }, controllers: { sessions: 'spree/admin/user_sessions', - passwords: 'spree/admin/user_passwords' + passwords: 'spree/admin/user_passwords', + unlocks: 'spree/admin/user_unlocks' }, router_name: :spree }) @@ -61,6 +62,9 @@ post '/password/recover', to: 'user_passwords#create', as: :reset_password get '/password/change', to: 'user_passwords#edit', as: :edit_password put '/password/change', to: 'user_passwords#update', as: :update_password + + get '/unlock', to: 'user_unlocks#show', as: :unlock + post '/unlock', to: 'user_unlocks#create' end end end diff --git a/lib/controllers/backend/spree/admin/user_unlocks_controller.rb b/lib/controllers/backend/spree/admin/user_unlocks_controller.rb new file mode 100644 index 00000000..cf711478 --- /dev/null +++ b/lib/controllers/backend/spree/admin/user_unlocks_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Spree + module Admin + class UserUnlocksController < Devise::UnlocksController + helper 'spree/base' + + include Spree::Core::ControllerHelpers::Auth + include Spree::Core::ControllerHelpers::Common + include Spree::Core::ControllerHelpers::Store + + helper 'spree/admin/navigation' + layout 'spree/layouts/admin' + + private + + def after_unlock_path_for(_resource) + admin_login_path if is_navigational_format? + end + end + end +end diff --git a/lib/views/backend/spree/user_mailer/unlock_instructions.html.erb b/lib/views/backend/spree/user_mailer/unlock_instructions.html.erb new file mode 100644 index 00000000..1a40d6ea --- /dev/null +++ b/lib/views/backend/spree/user_mailer/unlock_instructions.html.erb @@ -0,0 +1,7 @@ +

Hello <%= @user.email %>!

+ +

Your account has been locked due to an excessive number of unsuccessful sign in attempts.

+ +

Click the link below to unlock your account:

+ +

<%= link_to 'Unlock my account', @unlock_url %>

diff --git a/spec/controllers/spree/admin/user_unlocks_controller_spec.rb b/spec/controllers/spree/admin/user_unlocks_controller_spec.rb new file mode 100644 index 00000000..d497e817 --- /dev/null +++ b/spec/controllers/spree/admin/user_unlocks_controller_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +RSpec.describe Spree::Admin::UserUnlocksController, type: :controller do + # rubocop:disable RSpec/InstanceVariable + before { @request.env['devise.mapping'] = Devise.mappings[:spree_user] } + + describe '#create' do + let(:user) { create(:user, locked_at: Time.current) } + + it 'sends unlock instructions to the user' do + # rubocop:disable RSpec/StubbedMock + expect(Spree::UserMailer).to receive(:unlock_instructions).and_return(double(deliver: true)) + # rubocop:enable RSpec/StubbedMock + + post :create, params: { spree_user: { email: user.email } } + + expect(assigns[:spree_user].email).to eq(user.email) + expect(response.code).to eq('302') + end + end + + describe '#show' do + let(:user) { create(:user, locked_at: Time.current) } + + before { + @raw_token, encrypted_token = Devise.token_generator.generate(user.class, :unlock_token) + user.update(unlock_token: encrypted_token) + } + + it 'unlocks a previously locked user' do + get :show, params: { unlock_token: @raw_token } + + expect(response.code).to eq '302' + expect(user.reload.locked_at).to be_nil + end + end + + # rubocop:enable RSpec/InstanceVariable +end