用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, 就可以很轻松地查看是否正确发送了邮件了。