Commit 6c0ca261 by Son Do Hong

Merge branch 'password-reset' into 'master'

First commit

See merge request !11
parents ab0d9395 449dd2b7
class PasswordResetsController < ApplicationController
before_action :get_user, only: [:edit, :update]
before_action :valid_user, only: [:edit, :update]
before_action :check_expiration, only: [:edit, :update]
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")
render "edit"
elsif @user.update_attributes(user_params)
log_in @user
@user.update_attribute(:reset_digest, nil)
flash[:success] = "Password has been reset."
redirect_to @user
else
render "edit"
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
# Confirms a valid user.
def valid_user
return redirect_to root_url unless (@user && @user.activated? && @user.authenticated?(:reset, params[:id]))
end
# Checks expiration of reset token.
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
...@@ -4,8 +4,8 @@ class UserMailer < ApplicationMailer ...@@ -4,8 +4,8 @@ class UserMailer < ApplicationMailer
mail to: user.email, subject: "Account activation" mail to: user.email, subject: "Account activation"
end end
def password_reset def password_reset(user)
@greeting = "Hi" @user = user
mail to: "to@example.org" mail to: user.email, subject: "Password reset"
end end
end end
class User < ApplicationRecord class User < ApplicationRecord
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
attr_accessor :remember_token, :activation_token attr_accessor :remember_token, :activation_token, :reset_token
validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false } validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false }
validates :name, presence: true, length: { maximum: 50 } validates :name, presence: true, length: { maximum: 50 }
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
before_save { email.downcase! } #Call Backs before_save { email.downcase! } #Call Backs
before_save :downcase_email before_save :downcase_email
before_create :create_activation_digest before_create :create_activation_digest
has_secure_password has_secure_password
...@@ -60,6 +60,22 @@ class User < ApplicationRecord ...@@ -60,6 +60,22 @@ class User < ApplicationRecord
UserMailer.account_activation(self).deliver_now UserMailer.account_activation(self).deliver_now
end end
# Sets the password reset attributes.
def create_reset_digest
self.reset_token = User.new_token
update_columns(reset_digest: User.digest(reset_token), reset_sent_at: Time.current)
end
# Sends password reset email.
def send_password_reset_email
UserMailer.password_reset(self).deliver_now
end
# Returns true if a password reset has expired.
def password_reset_expired?
reset_sent_at < 2.hours.ago
end
private private
# Converts email to all lower-case. # Converts email to all lower-case.
......
<% provide(:title, "Reset password") %>
<h1>Reset password</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user, url: password_reset_path(params[:id])) 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>
<% provide(:title, "Forgot password") %>
<h1>Forgot password</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(:password_reset, url: password_resets_path) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: "form-control" %>
<%= f.submit "Submit", class: "btn btn-primary" %>
<% end %>
</div>
</div>
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
<%= f.label :email %> <%= f.label :email %>
<%= f.email_field :email, class: "form-control" %> <%= f.email_field :email, class: "form-control" %>
<%= f.label :password %> <%= f.label :password %>
<%= link_to "(forgot password)", new_password_reset_path %>
<%= f.password_field :password, class: "form-control" %> <%= f.password_field :password, class: "form-control" %>
<%= f.label :remember_me, class: "checkbox inline" do %> <%= f.label :remember_me, class: "checkbox inline" do %>
<%= f.check_box :remember_me %> <%= f.check_box :remember_me %>
......
<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> <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 password will stay as it is.
</p> </p>
User#password_reset To reset your password click the link below:
<%= @greeting %>, find me in app/views/user_mailer/password_reset.text.erb <%= 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.
...@@ -75,15 +75,15 @@ Rails.application.configure do ...@@ -75,15 +75,15 @@ Rails.application.configure do
# Use default logging formatter so that PID and timestamp are not suppressed. # Use default logging formatter so that PID and timestamp are not suppressed.
config.log_formatter = ::Logger::Formatter.new config.log_formatter = ::Logger::Formatter.new
# Use a different logger for distributed setups. # Use a different logger for distributed setups.
# require "syslog/logger" # require "syslog/logger"
# config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name")
if ENV["RAILS_LOG_TO_STDOUT"].present? if ENV["RAILS_LOG_TO_STDOUT"].present?
logger = ActiveSupport::Logger.new(STDOUT) logger = ActiveSupport::Logger.new(STDOUT)
logger.formatter = config.log_formatter logger.formatter = config.log_formatter
config.logger = ActiveSupport::TaggedLogging.new(logger) config.logger = ActiveSupport::TaggedLogging.new(logger)
end end
# Do not dump schema after migrations. # Do not dump schema after migrations.
......
...@@ -7,6 +7,9 @@ Rails.application.routes.draw do ...@@ -7,6 +7,9 @@ Rails.application.routes.draw do
get "/login", to: "sessions#new" get "/login", to: "sessions#new"
post "/login", to: "sessions#create" post "/login", to: "sessions#create"
delete "/logout", to: "sessions#destroy" delete "/logout", to: "sessions#destroy"
get "password_resets/new"
get "password_resets/edit"
resources :users resources :users
resources :account_activations, only: [:edit] resources :account_activations, only: [:edit]
resources :password_resets, only: [:new, :create, :edit, :update]
end end
class AddResetToUsers < ActiveRecord::Migration[5.1]
def change
add_column :users, :reset_digest, :string
add_column :users, :reset_sent_at, :datetime
end
end
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20191119063621) do ActiveRecord::Schema.define(version: 20191120021439) do
create_table "users", force: :cascade do |t| create_table "users", force: :cascade do |t|
t.string "name" t.string "name"
...@@ -23,6 +23,8 @@ ActiveRecord::Schema.define(version: 20191119063621) do ...@@ -23,6 +23,8 @@ ActiveRecord::Schema.define(version: 20191119063621) do
t.string "activation_digest" t.string "activation_digest"
t.boolean "activated", default: false t.boolean "activated", default: false
t.datetime "activated_at" t.datetime "activated_at"
t.string "reset_digest"
t.datetime "reset_sent_at"
t.index ["email"], name: "index_users_on_email", unique: true t.index ["email"], name: "index_users_on_email", unique: true
end 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"
# 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
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_sent_at, 3.hours.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/i, response.body
end
end
...@@ -12,6 +12,8 @@ class UserMailerPreview < ActionMailer::Preview ...@@ -12,6 +12,8 @@ class UserMailerPreview < ActionMailer::Preview
# Preview this email at # Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/password_reset # http://localhost:3000/rails/mailers/user_mailer/password_reset
def password_reset def password_reset
UserMailer.password_reset user = User.first
user.reset_token = User.new_token
UserMailer.password_reset(user)
end end
end end
...@@ -12,4 +12,15 @@ class UserMailerTest < ActionMailer::TestCase ...@@ -12,4 +12,15 @@ class UserMailerTest < ActionMailer::TestCase
assert_match user.activation_token, mail.body.encoded assert_match user.activation_token, mail.body.encoded
assert_match CGI.escape(user.email), mail.body.encoded assert_match CGI.escape(user.email), mail.body.encoded
end 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 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