diff --git a/app/models/user.rb b/app/models/user.rb index 38eb7911e..87652c380 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,7 +3,7 @@ class User < ApplicationRecord attribute :registering_from_web, default: false devise :database_authenticatable, :registerable, :confirmable, :recoverable, :rememberable, - :trackable, :validatable, :omniauthable, :password_expirable, :secure_validatable, + :trackable, :validatable, :omniauthable, :password_expirable, :secure_validatable, :lockable, authentication_keys: [:login] acts_as_voter diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index e297bfd48..58493f5b2 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -167,27 +167,27 @@ Devise.setup do |config| # 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 = :failed_attempts # Defines which key will be used when locking and unlocking an account - # config.unlock_keys = [:email] + config.unlock_keys = [:email] # 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. - # config.maximum_attempts = 20 + config.maximum_attempts = 20 # Time interval to unlock the account if :time is enabled as unlock_strategy. - # config.unlock_in = 1.hour + config.unlock_in = 1.hour # Warn on the last attempt before the account is locked. - # config.last_attempt_warning = true + config.last_attempt_warning = false # ==> Configuration for :recoverable # diff --git a/db/migrate/20231009073912_add_lockable_to_devise.rb b/db/migrate/20231009073912_add_lockable_to_devise.rb new file mode 100644 index 000000000..35bd75d3e --- /dev/null +++ b/db/migrate/20231009073912_add_lockable_to_devise.rb @@ -0,0 +1,11 @@ +class AddLockableToDevise < ActiveRecord::Migration[6.1] + def change + # Only if lock strategy is :failed_attempts + add_column :users, :failed_attempts, :integer, default: 0, null: false + add_column :users, :locked_at, :datetime + + # Add these only if unlock strategy is :email or :both + add_column :users, :unlock_token, :string + add_index :users, :unlock_token, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 603deabd5..db35e4c6c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1644,6 +1644,9 @@ ActiveRecord::Schema.define(version: 2023_10_12_141318) do t.boolean "recommended_debates", default: true t.boolean "recommended_proposals", default: true t.string "subscriptions_token" + t.integer "failed_attempts", default: 0, null: false + t.datetime "locked_at" + t.string "unlock_token" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["date_of_birth"], name: "index_users_on_date_of_birth" t.index ["email"], name: "index_users_on_email", unique: true @@ -1652,6 +1655,7 @@ ActiveRecord::Schema.define(version: 2023_10_12_141318) do t.index ["hidden_at"], name: "index_users_on_hidden_at" t.index ["password_changed_at"], name: "index_users_on_password_changed_at" t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true + t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true t.index ["username"], name: "index_users_on_username" end diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb new file mode 100644 index 000000000..57fecedb6 --- /dev/null +++ b/spec/controllers/users/sessions_controller_spec.rb @@ -0,0 +1,25 @@ +require "rails_helper" + +describe Users::SessionsController do + before { request.env["devise.mapping"] = Devise.mappings[:user] } + + let!(:user) { create(:user, email: "citizen@consul.org", password: "12345678") } + + describe "Devise lock" do + context "when devise sign in maximum_attempts reached", :with_frozen_time do + it "locks the user account and sends an email to the account with an unlock link" do + user.update(failed_attempts: 19) + + expect do + post :create, params: { user: { login: "citizen@consul.org", password: "wrongpassword" }} + end.to change { user.reload.failed_attempts }.by(1) + .and change { user.reload.locked_at }.from(nil).to(Time.current) + + expect(ActionMailer::Base.deliveries.count).to eq(1) + body = ActionMailer::Base.deliveries.last.body + expect(body).to have_content "Your account has been locked" + expect(body).to have_link "Unlock my account" + end + end + end +end