diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb index 0acd046..70e5487 100644 --- a/app/controllers/password_resets_controller.rb +++ b/app/controllers/password_resets_controller.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 1875cf8..591b6e4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/views/password_resets/edit.html.erb b/app/views/password_resets/edit.html.erb index 2d1800e..3887a78 100644 --- a/app/views/password_resets/edit.html.erb +++ b/app/views/password_resets/edit.html.erb @@ -1,2 +1,21 @@ -

PasswordResets#edit

-

Find me in app/views/password_resets/edit.html.erb

+<% provide(:title, "Reset password") %> +

Reset password

+ +
+
+ <%= 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 %> +
+
diff --git a/test/integration/password_resets_test.rb b/test/integration/password_resets_test.rb new file mode 100644 index 0000000..4260d69 --- /dev/null +++ b/test/integration/password_resets_test.rb @@ -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