feat: add password reset functionality
- Implement password reset request and form - Add user validation and expiration checks - Create integration tests for password reset process This commit introduces a complete password reset feature, allowing users to reset their passwords securely. It includes necessary validations to ensure the user is valid and the reset token has not expired. Additionally, integration tests have been added to verify the functionality and edge cases, enhancing overall application security and user experience.
This commit is contained in:
parent
8bb7615cb1
commit
32ec61fe00
@ -1,4 +1,9 @@
|
||||
class PasswordResetsController < ApplicationController
|
||||
before_action :get_user, only: [:edit, :update]
|
||||
before_action :valid_user, only: [:edit, :update]
|
||||
before_action :check_expiration, only: [:edit, :update]
|
||||
|
||||
include SessionsHelper
|
||||
def new
|
||||
end
|
||||
|
||||
@ -17,4 +22,44 @@ class PasswordResetsController < ApplicationController
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
if params[:user][:password].empty?
|
||||
@user.errors.add(:password, "can't be empty")
|
||||
render 'edit', status: :unprocessable_entity
|
||||
elsif @user.update(user_params)
|
||||
forget(@user)
|
||||
reset_session
|
||||
@user.update_attribute(:reset_digest, nil)
|
||||
log_in @user
|
||||
flash[:success] = "Password has been reset"
|
||||
redirect_to @user
|
||||
else
|
||||
render 'edit', status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:password, :password_confirmation)
|
||||
end
|
||||
|
||||
def get_user
|
||||
@user = User.find_by(email: params[:email])
|
||||
end
|
||||
|
||||
def valid_user
|
||||
unless @user && @user.activated? &&
|
||||
@user.authenticated?(:reset, params[:id])
|
||||
redirect_to root_url
|
||||
end
|
||||
end
|
||||
|
||||
def check_expiration
|
||||
if @user.password_reset_expired?
|
||||
flash[:danger] = "Password reset has expired"
|
||||
redirect_to new_password_reset_url
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -69,14 +69,18 @@ class User < ApplicationRecord
|
||||
|
||||
def create_reset_digest
|
||||
self.reset_token = User.new_token
|
||||
update_attribute(:reset_digest, User.digest(reset_token))
|
||||
update_attribute(:reset_sent_at, Time.zone.now)
|
||||
update_columns(reset_digest: User.digest(reset_token),
|
||||
reset_send_at: Time.zone.now)
|
||||
end
|
||||
|
||||
def send_password_reset_email
|
||||
UserMailer.password_reset(self).deliver_now
|
||||
end
|
||||
|
||||
def password_reset_expired?
|
||||
reset_send_at < 2.hours.ago
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def downcase_email
|
||||
|
@ -1,2 +1,21 @@
|
||||
<h1>PasswordResets#edit</h1>
|
||||
<p>Find me in app/views/password_resets/edit.html.erb</p>
|
||||
<% provide(:title, "Reset password") %>
|
||||
<h1>Reset password</h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<%= form_with(model: @user, url: password_reset_path(params[:id]),
|
||||
local: true) do |f| %>
|
||||
<%= render 'shared/error_messages' %>
|
||||
|
||||
<%= hidden_field_tag :email, @user.email %>
|
||||
|
||||
<%= f.label :password %>
|
||||
<%= f.password_field :password, class: 'form-control' %>
|
||||
|
||||
<%= f.label :password_confirmation, "Confirmation" %>
|
||||
<%= f.password_field :password_confirmation, class: "form-control" %>
|
||||
|
||||
<%= f.submit "Update password", class: "btn btn-primary" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
79
test/integration/password_resets_test.rb
Normal file
79
test/integration/password_resets_test.rb
Normal file
@ -0,0 +1,79 @@
|
||||
require "test_helper"
|
||||
|
||||
class PasswordResetsTest < ActionDispatch::IntegrationTest
|
||||
def setup
|
||||
ActionMailer::Base.deliveries.clear
|
||||
@user = users(:michael)
|
||||
end
|
||||
|
||||
test "password reset" do
|
||||
get new_password_reset_path
|
||||
assert_template "password_resets/new"
|
||||
assert_select "input[name=?]", "password_reset[email]"
|
||||
# email is invalid
|
||||
post password_resets_path, params: { password_reset: { email: "" } }
|
||||
assert_not flash.empty?
|
||||
assert_template "password_resets/new"
|
||||
# email is valid
|
||||
post password_resets_path,
|
||||
params: { password_reset: { email: @user.email } }
|
||||
assert_not_equal @user.reset_digest, @user.reload.reset_digest
|
||||
assert_equal 1, ActionMailer::Base.deliveries.size
|
||||
assert_not flash.empty?
|
||||
assert_redirected_to root_url
|
||||
# password reset form
|
||||
user = assigns(:user)
|
||||
# email is error
|
||||
get edit_password_reset_path(user.reset_token, email: "")
|
||||
assert_redirected_to root_url
|
||||
# user is inactivated
|
||||
user.toggle!(:activated)
|
||||
get edit_password_reset_path(user.reset_token, email: user.email)
|
||||
assert_redirected_to root_url
|
||||
user.toggle!(:activated)
|
||||
# email is right, token is wrong
|
||||
get edit_password_reset_path("wrong token", email: user.email)
|
||||
assert_redirected_to root_url
|
||||
# email is right, token is right
|
||||
get edit_password_reset_path(user.reset_token, email: user.email)
|
||||
assert_template "password_resets/edit"
|
||||
assert_select "input[name=email][type=hidden][value=?]", user.email
|
||||
# password is not patch
|
||||
patch password_reset_path(user.reset_token),
|
||||
params: { email: user.email,
|
||||
user: { password: "foobaz",
|
||||
password_confirmation: "barquux" } }
|
||||
assert_select "div#error_explanation"
|
||||
# password is empty
|
||||
patch password_reset_path(user.reset_token),
|
||||
params: { email: user.email,
|
||||
user: { password: "",
|
||||
password_confirmation: "" } }
|
||||
assert_select "div#error_explanation"
|
||||
# password and password_confirmation is valid
|
||||
patch password_reset_path(user.reset_token),
|
||||
params: { email: user.email,
|
||||
user: { password: "foobaz",
|
||||
password_confirmation: "foobaz" } }
|
||||
assert is_logged_in?
|
||||
assert_not flash.empty?
|
||||
assert_redirected_to user
|
||||
assert_nil user.reload.reset_digest
|
||||
end
|
||||
|
||||
test "expired token" do
|
||||
get new_password_reset_path
|
||||
post password_resets_path,
|
||||
params: { password_reset: { email: @user.email } }
|
||||
|
||||
@user = assigns(:user)
|
||||
@user.update_attribute(:reset_send_at, 3.hour.ago)
|
||||
patch password_reset_path(@user.reset_token),
|
||||
params: { email: @user.email,
|
||||
user: { password: "foobar",
|
||||
password_confirmation: "foobar" } }
|
||||
assert_response :redirect
|
||||
follow_redirect!
|
||||
assert_match "expired", response.body
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user