用devise做用户登录,遇到的一个问题,记录下场景和leader指导出的解决方法。
场景:
添加项目成员member时,需要管理员新增用户user。
- 管理员在新增用户页面中输入邮箱;
 - 管理员提交表单,用户的初始密码随机生成,随后给新建的用户发送一份账户激活的邮件;
 - 新增用户登录注册的邮箱,点击邮件中的链接【看似激活,实则为重置密码的链接】;
 - 用户重置密码完成,自动登录。
 
这里只涉及devise如何发送重置密码的邮件。
准备工作:
三个model:
对应的表结构:
# == Schema Information
#
# Table name: members
#
#  id         :integer          not null, primary key
#  user_id    :integer
#  project_id :integer
#  created_at :datetime         not null
#  updated_at :datetime         not null
# Table name: projects
#
#  id         :integer          not null, primary key
#  title      :string
#  created_at :datetime         not null
#  updated_at :datetime         not null
# Table name: users
#
#  id                     :integer          not null, primary key
#  email                  :string           default(""), not null
#  encrypted_password     :string           default(""), not null
#  reset_password_token   :string
#  reset_password_sent_at :datetime
#  remember_created_at    :datetime
#  sign_in_count          :integer          default(0), not null
#  current_sign_in_at     :datetime
#  last_sign_in_at        :datetime
#  current_sign_in_ip     :string
#  last_sign_in_ip        :string
#  created_at             :datetime         not null
#  updated_at             :datetime         not null
解决:
使用devise自带的sessions。在routes.rb中做如下修改:
### config/routes.rb
devise_for :users, path: "", controllers: {
    sessions: "sessions"
  }
新建一个sessionscontroller:rails g controller sessions
 继承Devise::SessionsController.
### app/controllers/sessions_controller.rb
class SessionsController < Devise::SessionsController
end
如果希望root页面就是显示注册的页面,可以在routes.rb中添加如下代码:
### config/routes.rb
 devise_scope :user do
    get "/", controller: :sessions, action: :new
  end
激活账户涉及confirmable模块,使用rails g devise user 创建user时,默认没有confirmable的,需要自己手动添加,请在创建user时,确保含有以下字段:
t.string   :confirmation_token
t.datetime :confirmed_at
t.datetime :confirmation_sent_at
t.string   :unconfirmed_email # Only if using reconfirmable
「如果已经创建,使用rail g migration,给user添加上这些字段,同时迁移数据,给现有用户的confirmed_at赋值,不然无法登录。」
修改user.rb, 添加上模块::confirmable
### app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :rememberable, :validatable, :recoverable, :confirmable
  ......
end
同时在新增成员的表单中,允许输入用户的邮箱,如果邮箱存在,则直接添加该用户到项目成员中,如果邮箱不存在,则给该邮箱发送确认的邮件。
修改member.rb, 添加:
### app/models/member.rb
arr_accessor :email
这样member的new.html.erb中就可以使用f.input :email,随后在controller的create action中,通过传过来的email来判断用户是否已经存在。修改create中原来的@member.save,改成@member.submit, 在member.rb中添加submit 函数:
### app/models/member.rb
  def submit(email)
    password = SecureRandom.hex(4)
    user = User.where(email: email).first_or_initialize(password: password, password_confirmation: password)
    user.save
    self.user = user
    self.save
    errors.empty?
  end
### 这里省略了errors的delegate,如果email是无效的,需要将user的error delegate给member。
在members_controller中,修改create action:
#### app/controllers/projects/members_controller.rb
 def create
    if @member.submit(member_params[:email])
      @user = @member.user
      @user.send_activation_instructions unless @user.confirmed?
    end
  end
而对于send_activation_instructions,发送确认邮件的method,可以参考send_devise_notification 在 devise 中的定义:
### app/models/authenticatable.rb
def send_devise_notification(notification, *args)
  message = devise_mailer.send(notification, self, *args)
  # Remove once we move to Rails 4.2+ only.
  if message.respond_to?(:deliver_now)
    message.deliver_now
  else
    message.deliver
  end
end
这里message = devise_mailer.send(notification, self, *args),devise_mailer使用send,调用参数notification传过来的方法名。
看Devise::Mailer,里面定义了5个methods, 用于发送不同类型的邮件需求:
#### app/mailers/devise/mailer.rb
## 以下是confirmation_instructions。
def confirmation_instructions(record, token, opts={})
  @token = token
  devise_mail(record, :confirmation_instructions, opts)
end
.......
参考这个,我们可以写出activation_instructions。
在 user.rb中,定义: send_activation_instructions。
### app/models/user.rb
def send_activation_instructions
  token = set_reset_password_token ## 生成 reset_password_token,赋给token变量
  send_devise_notification(:activation_instructions, token, {})
end
新增一个mailer: UserMailer,继承 devise_mailer, 定义 activation_instructions。
### app/mailers/user_mailer.rb
class UserMailer < Devise::Mailer
  def activation_instructions(record, token, opts = {})
    @token = token
    devise_mail(record, :activation_instructions, opts)
  end
end
修改user.rb,添加:
### app/models/user.rb
def devise_mailer
  UserMailer
end
同时,设置 reconfirmable 为false:
### app/models/user.rb
  devise :database_authenticatable, :rememberable, :trackable, :validatable, :recoverable, :confirmable, reconfirmable: false
  ......
这样在member调用submit时,user.save就不会抛出异常,要求confirm这个账户了。
在user_mailer的添加activation_instructions.html.erb 文件,文件内容可参考devise的confirmation_instructions.html.erb.
### app/views/user_mailer/activation_instructions.html.erb
<p><%= t('.greeting', recipient: @resource.name) %></p>
<p><%= t('.instruction') %></p>
<p><%= link_to t('.action'), edit_reset_password_url(reset_password_token: @token) %></p>
这里注意把confirmation_instructions.html.erb中,confirmation_url替换成 edit_reset_password_url,因为我们希望的是用户点击这个链接,跳转到密码修改的页面。
最后,把reset_password这个功能添加进来。
修改routes.rb,跳过devise自带的confirmations和passwords,用reset_password来完成验证邮箱和密码重置的功能,添加:
### config/routes.rb devise_for :users, path: "", controllers: { sessions: "sessions" }, skip: [ :confirmations, :passwords ] devise_scope :user do resource :reset_password end新建controller,继承自devise的passwordsController:
class ResetPasswordsController < Devise::PasswordsController def update super do |resource| resource.confirm if resource.errors.empty? && !resource.confirmed? end end end这里重新定义了update, 调用super时,传递了一个block,如果user 没有confirmed过,同时更新密码成功,就调用confirm。在修改密码的同时,完成了用户的邮箱验证。
在app/views/reset_passwords下,添加edit.html.erb,用户收到邮件时,点击“确认我的账户”时,会跳转到该页面,在该页面进行密码重置。具体页面内容,此处略。
BTW,有关邮箱的配置,可以参考rails的官方文档。在开发环境下,推荐安装 letter_opener这个gem, 就可以很轻松地查看是否正确发送了邮件了。