踩坑 Devise 和 cancancan。
场景
使用devise做用户管理,cancancan做权限管理,生成model user,user含有name,avatar等字段,现在需要让user可以修改自己的密码,姓名,头像等。即:
- 用户可以不修改密码,只修改姓名或者头像
- 用户修改密码时,需要填写原来的密码来进行验证,同时新密码需要输入两次以确认
也就是,当用户输入current_password, password, password_confirmation中的任意一个时,就进行相关的密码验证,如果三者均为空,则不更新密码部分,更新其他属性。
实现步骤
在修改代码前,需要知道这两点:
使用devise作用户管理时,如果你的
app/models/user.rb
中,devise添加了validatable:devise :xxxx, :..., :validatable
那么相当于给user添加了如下的validates:
validates_uniqueness_of :email, allow_blank: true, if: :email_changed?
validates_format_of :email, with: email_regexp, allow_blank: true, if: :email_changed?
validates_presence_of :password, if: :password_required?
validates_confirmation_of :password, if: :password_required?
validates_length_of :password, within: password_length, allow_blank: true
这里注意到validates_presence_of :password, if: :password_required?
,当password_required?为true时,password必填。password_required?这个method的源代码如下:
def password_required?
!persisted? || !password.nil? || !password_confirmation.nil?
end
如果更新的时候,我们不更新密码部分,只更新姓名或者头像,就会报错,因为此时表单送出的password是“”,即空值,但不是nil,这样 !password.nil? 就会为true,导致password_required?为true,所以我们需要重写这个method,当password为空时,也可以更新用户信息。
在app/models/user.rb
中,添加如下代码:
def password_required?
new_record? || password.present? || password_confirmation.present?
end
这样添加后,当你修改用户信息时,如果password那一栏没有填写,不会出现如下报错:
修改密码时,因为需要输入当前的密码,也就是current_password这个字段,而原先user中是没有添加这个字段的,所以需要在
app/models/user.rb
中,添加如下代码,保证current_password可读写。attr_writer :current_password
好了,准备工作完成,算是解决了大半了,我们的重头戏来了。
新增一个controller account::user 来让让用户完成修改密码的操作:
rails g controller account::user
在routes.rb
中添加路径:【因为修改密码是用户单个个体的行为,所以我们使用resource】
namespace :account do
resource :user
end
我们添加上view,让用户可以修改个人信息:
touch app/views/account/users/show.html.erb
在 app/views/account/users/show.html.erb
中添加如下内容:
<%= simple_form_for @user, url: { action: :update } do |f| %>
<%= f.input :name %>
<%= f.file_field :avatar %>
<%= f.input :current_password %>
<%= f.input :password %>
<%= f.input :password_confirmation %>
<%= f.submit "更新", class: "btn btn-success"%>
<% end %>
同时需要在其他页面,比如navbar中提供入口,让用户可以进入该页面更新个人信息,这里略去。
在app/controllers/account/users_controller.rb
中,添加如下内容:
class Account::UsersController < ApplicationController
authorize_resource :user, parent: false
def show
@user = User.find(current_user.id)
end
def update
@user = User.find(current_user.id)
if user_params.values_at(:current_password, :password, :password_confirmation).any?(&:present?)
@user.update_with_password(user_params)
else
@user.update(user_params)
end
redirect_to profile_path
end
protected
def user_params
params.require(:user).permit(:name, :avatar, :current_password, :password, :password_confirmation)
end
end
其中,因为使用cancancan做权限管理,所以这里我们用authorize_resource :user, parent: false
来给user授权authorize,让user可以有读写的权利, update action部分中,我们使用了devise的instance method update_with_password来修改密码,它会检查用户的:current_password, :password, :password_confirmation这三者的有效性。
上面的代码看着有些不够简洁,我们稍稍重构下:
把@user = User.find(current_user.id)拎出来,放在before action里面,把[:current_password, :password, :password_confirmation]这三个密码相关的变量用一个method包起来。
class Account::UsersController < ApplicationController
before_action -> { @user = User.find(current_user.id) }
authorize_resource :user, parent: false
def show
end
def update
if user_params.values_at(*password_param_lists).any?(&:present?)
@user.update_with_password(user_params)
else
@user.update(user_params)
end
redirect_to profile_path
end
protected
def user_params
params.require(:user).permit(:name, :avatar, *password_param_lists)
end
def password_param_lists
[:current_password, :password, :password_confirmation]
end
end
OK! 基本功能实现。