Commit 6c7469e0 by Mai Hoang Thai Ha

Add password reset

parent 93a26287
Pipeline #1286 failed with stages
in 0 seconds
......@@ -124,9 +124,6 @@ GEM
mini_mime (>= 0.1.1)
marcel (1.0.1)
method_source (1.0.0)
mimemagic (0.3.10)
nokogiri (~> 1)
rake
mini_mime (1.0.3)
minitest (5.14.4)
minitest-reporters (1.3.8)
......@@ -264,7 +261,6 @@ DEPENDENCIES
guard-minitest (= 2.4.6)
jbuilder (~> 2.7)
listen (~> 3.3)
mimemagic (~> 0.3.10)
minitest (= 5.14.4)
minitest-reporters (= 1.3.8)
pg (= 1.1.4)
......
// Place all the styles related to the PasswordResets controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: https://sass-lang.com/
class PasswordResetsController < ApplicationController
before_action :get_user, only: [:edit, :update]
before_action :valid_user, only: [:edit, :update]
before_action :check_expiration, only: [:edit, :update] #Case 1
def new
end
def create
@user = User.find_by(email: params[:password_reset][:email].downcase)
if @user
@user.create_reset_digest
@user.send_password_reset_email
flash[:info] = "Email sent with password reset instructions"
redirect_to root_url
else
flash.now[:danger] = "Email address not found"
render 'new'
end
end
def edit
end
def update
if params[:user][:password].empty?
@user.errors.add(:password, "can't be empty") # Case 3
render 'edit'
elsif @user.update(user_params) # Case 4
log_in @user
flash[:success] = "Password has been reset."
redirect_to @user
else
render 'edit' # Case 2
end
end
private
def user_params
params.require(:user).permit(:password, :password_confirmation)
end
# Before filters
def get_user
@user = User.find_by(email: params[:email])
end
# Confirms a valid user
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
module PasswordResetsHelper
end
......@@ -16,9 +16,8 @@ class UserMailer < ApplicationMailer
#
# en.user_mailer.password_reset.subject
#
def password_reset
@greeting = "Hi"
mail to: "to@example.org"
def password_reset(user)
@user = user
mail to: user.email, subject: "Password reset"
end
end
class User < ApplicationRecord
attr_accessor :remember_token, :activation_token
attr_accessor :remember_token, :activation_token, :reset_token
before_save :downcase_email
before_create :create_activation_digest
validates :name, presence: true, length: { maximum:50 }
......@@ -51,6 +51,23 @@ class User < ApplicationRecord
UserMailer.account_activation(self).deliver_now
end
# Sets the password reset attributes
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)
end
# Sends password reset email
def send_password_reset_email
UserMailer.password_reset(self).deliver_now
end
# Return true if a password reset has expired
def password_reset_expired?
reset_sent_at <2.hours.ago
end
private
# Convaerts email to all lowr_case
......
<% provide(:title, 'Reset password') %>
<h1>Reset passwrod</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>
\ No newline at end of file
<% provide(:title, "Forgot password") %>
<h1>Forgot password</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_with(url: password_resets_path, scope: :password_reset, local: true) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.submit "Submit", class: "btn btn-primary" %>
<% end %>
</div>
</div>
\ No newline at end of file
......@@ -8,6 +8,7 @@
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= link_to "(forgot password)", new_password_reset_path %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :remember_me, class: "checkbox inline" do %>
......
<h1>User#password_reset</h1>
<h1>Password reset</h1>
<p>To reset your password click the link below:</p>
<%= link_to "Reset password", edit_password_reset_url(@user.reset_token,
email: @user.email) %>
<p>This link will expire in two hours.</p>
<p>
<%= @greeting %>, find me in app/views/user_mailer/password_reset.html.erb
If you did not request your password to be reset, please ignore this email and
your pa
</p>
\ No newline at end of file
User#password_reset
<%= @greeting %>, find me in app/views/user_mailer/password_reset.text.erb
To reset your password click the link below:
<%= edit_password_reset_url(@user.reset_token, email: @user.email) %>
This link will expire in two hours.
If you did not request your password to be reset, please ignore this email and
your password will stay as it is.
\ No newline at end of file
Rails.application.routes.draw do
get 'password_resets/new'
get 'password_resets/edit'
get 'sessions/new'
get 'users/new'
root 'static_pages#home'
......@@ -11,4 +13,5 @@ Rails.application.routes.draw do
delete '/logout', to: 'sessions#destroy'
resources :users
resources :account_activations, only: [:edit]
resources :password_resets, only: [:new, :create, :edit, :update]
end
class AddResetToUsers < ActiveRecord::Migration[6.1]
def change
add_column :users, :reset_digest, :string
add_column :users, :reset_sent_at, :datetime
end
end
......@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2021_06_16_070505) do
ActiveRecord::Schema.define(version: 2021_06_17_081805) do
create_table "users", force: :cascade do |t|
t.string "name"
......@@ -23,6 +23,8 @@ ActiveRecord::Schema.define(version: 2021_06_16_070505) do
t.string "activation_digest"
t.boolean "activated", default: false
t.datetime "activated_at"
t.string "reset_digest"
t.datetime "reset_sent_at"
t.index ["email"], name: "index_users_on_email", unique: true
end
......
require "test_helper"
class PasswordResetsTest < ActionDispatch::IntegrationTest
def setup
ActionMailer::Base.deliveries.clear
@user = users(:michael)
end
test "password resets" do
get new_password_reset_path
assert_template 'password_resets/new'
assert_select 'input[name=?]', 'password_reset[email]'
# Invalid email
post password_resets_path, params: { password_reset: { email: "" } }
assert_not flash.empty?
assert_template 'password_resets/new'
# Valid email
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)
# Wrong email
get edit_password_reset_path(user.reset_token, email: "")
assert_redirected_to root_url
# Inactive user
user.toggle!(:activated)
get edit_password_reset_path(user.reset_token, email: user.email)
assert_redirected_to root_url
user.toggle!(:activated)
# Right email, wrong token
get edit_password_reset_path('wrong token', email: user.email)
assert_redirected_to root_url
# Right email, right token
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
# Invalid password & confirmation
patch password_reset_path(user.reset_token),
params: { email: user.email,
user: { password: "foobaz",
password_confirmation: "barquux" } }
assert_select 'div#error_explanation'
# Empty password
patch password_reset_path(user.reset_token),
params: { email: user.email,
user: { password: "",
password_confirmation: "" } }
assert_select 'div#error_explanation'
# Valid password & confirmation
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
end
end
......@@ -10,7 +10,8 @@ class UserMailerPreview < ActionMailer::Preview
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/password_reset
def password_reset
UserMailer.password_reset
user = User.first
user.reset_token = User.new_token
UserMailer.password_reset(user)
end
end
......@@ -12,4 +12,15 @@ class UserMailerTest < ActionMailer::TestCase
assert_match user.activation_token, mail.body.encoded
assert_match CGI.escape(user.email), mail.body.encoded
end
test "password_reset" do
user = users(:michael)
user.reset_token = User.new_token
mail = UserMailer.password_reset(user)
assert_equal "Password reset", mail.subject
assert_equal [user.email], mail.to
assert_equal ["noreply@example.com"], mail.from
assert_match user.reset_token, mail.body.encoded
assert_match CGI.escape(user.email), mail.body.encoded
end
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment