经过一个寒假的休整(浪里个浪)又写了一段时间毕设和论文,终于重启了我的ror(Ruby on rails)作业。感觉已经没法再陌生了,这都是些什么鬼?(WTF ...)
创建及合并分支:略
资源
添加控制器
rails generate controller PasswordResets new edit --no-test-framework
添加路由
# config/routes.rb
... ...
resources :password_resets, only: [:new, :create, :edit, :update]
... ...
添加链接
# app/views/sessions/new.html.erb
... ...
<%= f.label :password %>
<%= link_to "(forgot password)", new_password_reset_path %>
<%= f.password_field :password, class: 'form-control' %>
... ...
添加迁移
rails generate migration add_reset_to_users reset_digest:string reset_sent_at:datetime
执行迁移
rails db:migrate
控制器和表单
重设密码页面视图
# app/views/password_resets/new.html.erb
<% provide(:title, "Forgot password") %>
Forgot password
<%= form_for(:password_reset, url: password_resets_path) do |f| %>
<%= f.label :email %>
<%= f.text_field :email, class: 'form-control' %>
<%= f.submit "Submit", class: "btn btn-primary" %>
<% end %>
控制器create动作
# app/controllers/password_resets_controller.rb
... ...
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
... ...
模型中添加辅助方法
# app/models/user.rb
... ...
attr_accessor :remember_token, :activation_token, :reset_token
... ...
# 设置密码重设相关属性
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
# 发送密码重设邮件
def send_password_reset_email
UserMailer.password_reset(self).deliver_now
end
... ...
邮件程序
发送密码重设链接
# app/mailers/user_mailer.rb
... ...
def password_reset(user)
@user = user
mail to: user.email, subject: "Password reset | From Microblog"
end
密码重设邮件视图
# app/views/user_mailler/password_reset.html.erb
Password reset
To reset your password click the link below:
<%= link_to "Reset password", edit_password_reset_url(@user.reset_token, email: @user.email, host: 'localhost:3000') %>
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.
# 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, host: 'localhost:3000') %>
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.
邮件预览
# test/mailers/previews/user_mailer_preview.rb
... ...
def password_reset
user = User.first
user.reset_token = User.new_token
UserMailer.password_reset(user)
end
... ...
邮件测试
# test/mailers/user_mailer_test.rb
... ...
test "password_reset" do
user = users(:joshua)
user.reset_token = User.new_token
mail = UserMailer.password_reset(user)
assert_equal "Password reset | From Microblog", mail.subject
assert_equal [user.email], mail.to
assert_equal "noreply@localhost:3000", mail.from
assert_match user.reset_token, mail.body.encoded
assert_match CGI::escape(user.email), mail.body.encoded
end
... ...
重设密码
添加表单
# app/views/password_resets/edit.html.erb
<% provide(:title, 'Reset password') %>
Reset password
<%= 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 %>
修改密码
# app/controllers/password_resets_controller.rb
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 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 both_passwords_blank?
flash.now[:danger] = "Password/confirmation can't be blank"
render 'edit'
elsif @user.update_attributes(user_params)
log_in @user
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 both_passwords_blank?
params[:user][:password].blank? && params[:user][:password_confirmation].blank?
end
# 检查令牌过期
def check_expiration
if @user.password_reset_expired?
flash[:danger] = "Password reset has expired."
redirect_to new_password_reset_url
end
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
end
判断令牌失效
# app/models/user.rb
... ...
def password_reset_expired?
reset_sent_at < 2.hours.ago
end
... ...
测试
生成测试文件
rails generate integration_test password_resets
编写测试
# test/integration/password_resets_test.rb
... ...
def setup
ActionMailer::Base.deliveries.clear
@user = users(:joshua)
end
test "password resets" do
get new_password_reset_path
assert_template 'password_resets/new'
# 邮件地址无效
post password_resets_path, password_reset: { email: "" }
assert_not flash.empty?
assert_template 'password_resets/new'
# 邮件地址有效
post password_resets_path, 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
# 密码重设表单
user = assigns(:user)
# 邮件地址错误
get edit_password_reset_path(user.reset_token, email: "")
assert_redirected_to root_url
# 用户未激活
user.toggle!(:activated)
get edit_password_reset_path(user.reset_token, email: user.email)
assert_redirected_to root_url
user.toggle!(:activated)
# 邮件地址正确,令牌不对
get edit_password_reset_path('wrong token', email: user.email)
assert_redirected_to root_url
# 邮件地址正确,令牌也对
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
# 密码和确认不匹配
patch password_reset_path(user.reset_token),
email: user.email, user: { password: "foobaz", password_confirmation: "barquux" }
assert_select 'div#error_explanation'
# 密码和确认都为空
patch password_reset_path(user.reset_token),
email: user.email, user: { password: " ", password_confirmation: " " }
assert_not flash.empty?
assert_template 'password_resets/edit'
# 密码和确认都有效
patch password_reset_path(user.reset_token),
email: user.email, user: { password: "foobaz", password_confirmation: "foobaz" }
assert is_logged_in?
assert_not flash.empty?
assert_redirected_to user
end
end
执行测试
rails test:integration
生产环境中发送邮件
添加SendGrid扩展
heroku addons:add sendgrid:starter
Creating sendgrid:starter on ⬢ fathomless-shelf-38262... !
▸ Please verify your account to install this add-on plan (please enter a credit card) For more information, see
▸ https://devcenter.heroku.com/categories/billing Verify now at https://heroku.com/verify
当然,需要账户填写信用卡信息(由于本人尚未申请信用卡,本部分略过)