# 9.4 刪除用戶
至此,用戶列表頁面完成了。符合 REST 架構的用戶資源只剩下最后一個了——`destroy` 動作。本節,我們會先添加刪除用戶的鏈接(構思圖如[圖 9.13](#fig-user-index-delete-links-mockup) 所示),然后再編寫 `destroy` 動作,完成刪除操作。不過,首先我們要先創建管理員級別的用戶,并授權這些用戶執行刪除操作。
圖 9.13:顯示有刪除鏈接的用戶列表頁面構思圖
## 9.4.1 管理員
我們要通過用戶模型中一個名為 `admin` 的屬性來判斷用戶是否具有管理員權限。`admin` 屬性的類型為布爾值,Active Record 會自動生成一個 `admin?` 方法,返回布爾值,判斷用戶是否為管理員。添加 `admin` 屬性后,用戶數據模型如[圖 9.14](#fig-user-model-admin) 所示。
圖 9.14:添加 `admin` 布爾值屬性后的用戶模型
和之前一樣,我們要使用遷移添加 `admin` 屬性,并且在命令行中指定其類型為 `boolean`:
```
$ rails generate migration add_admin_to_users admin:boolean
```
這個遷移會在 `users` 表中添加 `admin` 列,如[代碼清單 9.50](#listing-admin-migration) 所示。注意,在[代碼清單 9.50](#listing-admin-migration) 中我們在 `add_column` 方法中指定了 `default: false` 參數,意思是默認情況下用戶不是管理員。(如果不指定 `default: false` 參數,`admin` 的默認值是 `nil`,也是假值,所以這個參數并不是必須的。不過,指定這個參數,可以更明確地向 Rails 以及代碼的閱讀者表明這段代碼的意圖。)
##### 代碼清單 9.50:向用戶模型中添加 `admin` 屬性的遷移
db/migrate/[timestamp]_add_admin_to_users.rb
```
class AddAdminToUsers < ActiveRecord::Migration
def change
add_column :users, :admin, :boolean, default: false end
end
```
然后,像往常一樣,執行遷移:
```
$ bundle exec rake db:migrate
```
和預想的一樣,Rails 能自動識別 `admin` 屬性的類型為布爾值,自動生成 `admin?` 方法:
```
$ rails console --sandbox
>> user = User.first
>> user.admin?
=> false
>> user.toggle!(:admin)
=> true
>> user.admin?
=> true
```
這里,我們使用 `toggle!` 方法把 `admin` 屬性的值由 `false` 改為 `true`。
最后,我們要修改生成示例用戶的代碼,把第一個用戶設為管理員,如[代碼清單 9.51](#listing-populator-with-admin) 所示。
##### 代碼清單 9.51:在生成示例用戶的代碼中把第一個用戶設為管理員
db/seeds.rb
```
User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar",
admin: true)
99.times do |n|
name = Faker::Name.name
email = "example-#{n+1}@railstutorial.org"
password = "password"
User.create!(name: name,
email: email,
password: password,
password_confirmation: password)
end
```
然后重新創建數據庫:
```
$ bundle exec rake db:migrate:reset
$ bundle exec rake db:seed
```
### “健壯參數”再探
你可能注意到了,在[代碼清單 9.51](#listing-populator-with-admin) 中,我們在初始化哈希參數中指定了 `admin: true`,把用戶設為管理員。這么做的后果是,用戶對象暴露在網絡中了,如果在請求中提供初始化參數,惡意用戶就可以發送如下的 `PATCH` 請求:[[10](#fn-10)]
```
patch /users/17?admin=1
```
這個請求會把 17 號用戶設為管理員——這是個嚴重的潛在安全隱患。
鑒于此,必須只允許通過請求傳入可安全編輯的屬性。我們在 [7.3.2 節](chapter7.html#strong-parameters)說過,可以使用“健壯參數”實現這一限制,即在 `params` 哈希上調用 `require` 和 `permit` 方法:
```
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
```
注意,`admin` 并不在允許使用的屬性列表中。這樣就可以避免用戶取得網站的管理權。因為這一步很重要,最好再為不可編輯的屬性編寫一個測試。針對 `:admin` 屬性的測試留作[練習](#updating-showing-and-deleting-users-exercises)。
## 9.4.2 `destroy` 動作
完成用戶資源的最后一步是,添加刪除鏈接和 `destroy` 動作。我們先在用戶列表頁面每個用戶后面加入一個刪除鏈接,而且限制只有管理員才能執行刪除操作。只有當前用戶是管理員才能看到刪除鏈接。視圖如[代碼清單 9.52](#listing-delete-links) 所示。
##### 代碼清單 9.52:刪除用戶的鏈接(只有管理員能看到)
app/views/users/_user.html.erb
```
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
<% if current_user.admin? && !current_user?(user) %>
| <%= link_to "delete", user, method: :delete,
data: { confirm: "You sure?" } %><% end %> </li>
```
注意 `method: delete` 參數,它指明點擊鏈接后發送的是 `DELETE` 請求。我們還把鏈接放在了 `if` 語句中,這樣就只有管理員才能看到刪除用戶的鏈接。管理員看到的頁面如[圖 9.15](#fig-index-delete-links-rails-3) 所示。
圖 9.15:顯示有刪除鏈接的用戶列表頁面
瀏覽器不能發送 `DELETE` 請求,Rails 通過 JavaScript 模擬。也就是說,如果用戶禁用了 JavaScript,那么刪除用戶的鏈接就不可用了。如果必須要支持沒啟用 JavaScript 的瀏覽器,可以使用一個發送 `POST` 請求的表單來模擬 `DELETE` 請求,這樣即使禁用了 JavaScript,刪除用戶的鏈接仍能使用。[[11](#fn-11)]
為了讓刪除鏈接起作用,我們要定義 `destroy` 動作([表 7.1](chapter7.html#table-restful-users))。在 `destroy` 動作中,先找到要刪除的用戶,然后使用 Active Record 提供的 `destroy` 方法將其刪除,最后再重定向到用戶列表頁面,如[代碼清單 9.53](#listing-destroy-action) 所示。因為登錄后才能刪除用戶,所以[代碼清單 9.53](#listing-destroy-action) 還在 `logged_in_user` 事前過濾器中添加了 `:destroy`。
##### 代碼清單 9.53:添加 `destroy` 動作
app/controllers/users_controller.rb
```
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update, :destroy] before_action :correct_user, only: [:edit, :update]
.
.
.
def destroy
User.find(params[:id]).destroy flash[:success] = "User deleted" redirect_to users_url end
.
.
.
end
```
注意,在 `destroy` 動作中,我們把 `find` 方法和 `destroy` 方法連在一起調用,只占了一行:
```
User.find(params[:id]).destroy
```
理論上,只有管理員才能看到刪除用戶的鏈接,所以只有管理員才能刪除用戶。但實際上還是存在一個嚴重的安全漏洞:只要攻擊者有足夠的經驗,就可以在命令行中發送 `DELETE` 請求,刪除網站中的任何用戶。為了保障網站的安全,我們還要限制對 `destroy` 動作的訪問,只讓管理員刪除用戶。
和 [9.2.1 節](#requiring-logged-in-users)和 [9.2.2 節](#requiring-the-right-user)的做法一樣,我們要使用事前過濾器限制訪問。這一次,我們要限制只有管理員才能訪問 `destroy` 動作。我們要定義一個名為 `admin_user` 的事前過濾器,如[代碼清單 9.54](#listing-admin-destroy-before-filter) 所示。
##### 代碼清單 9.54:限制只有管理員才能訪問 `destroy` 動作的事前過濾器
app/controllers/users_controller.rb
```
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
before_action :correct_user, only: [:edit, :update]
before_action :admin_user, only: :destroy .
.
.
private
.
.
.
# 確保是管理員
def admin_user
redirect_to(root_url) unless current_user.admin? end
end
```
## 9.4.3 刪除用戶的測試
像刪除用戶這種危險的操作,一定要編寫測試,確保表現和預期一樣。首先,我們把一個用戶固件設為管理員,如[代碼清單 9.55](#listing-fixture-user-admin) 所示。
##### 代碼清單 9.55:把一個用戶固件設為管理員
test/fixtures/users.yml
```
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
admin: true
archer:
name: Sterling Archer
email: duchess@example.gov
password_digest: <%= User.digest('password') %>
lana:
name: Lana Kane
email: hands@example.gov
password_digest: <%= User.digest('password') %>
malory:
name: Malory Archer
email: boss@example.gov
password_digest: <%= User.digest('password') %>
<% 30.times do |n| %>
user_<%= n %>:
name: <%= "User #{n}" %>
email: <%= "user-#{n}@example.com" %>
password_digest: <%= User.digest('password') %>
<% end %>
```
按照 [9.2.1 節](#requiring-logged-in-users)的做法,我們會把限制訪問動作的測試放在用戶控制器的測試文件中。和[代碼清單 8.28](chapter8.html#listing-user-logout-test) 一樣,我們要使用 `delete` 方法直接向 `destroy` 動作發送 `DELETE` 請求。我們要檢查兩種情況:其一,沒登錄的用戶會重定向到登錄頁面;其二,已經登錄的用戶,但不是管理員,會重定向到首頁。測試如[代碼清單 9.56](#listing-action-tests-admin) 所示。
##### 代碼清單 9.56:測試只有管理員能訪問的動作 GREEN
test/controllers/users_controller_test.rb
```
require 'test_helper'
class UsersControllerTest < ActionController::TestCase
def setup
@user = users(:michael)
@other_user = users(:archer)
end
.
.
.
test "should redirect destroy when not logged in" do
assert_no_difference 'User.count' do
delete :destroy, id: @user
end
assert_redirected_to login_url
end
test "should redirect destroy when logged in as a non-admin" do
log_in_as(@other_user)
assert_no_difference 'User.count' do
delete :destroy, id: @user
end
assert_redirected_to root_url
end
end
```
注意,在[代碼清單 9.56](#listing-action-tests-admin) 中,我們使用 `assert_no_difference` 方法([代碼清單 7.21](chapter7.html#listing-a-test-for-invalid-submission) 中用過)確認用戶的數量沒有變化。
[代碼清單 9.56](#listing-action-tests-admin) 中的測試確認了未授權的用戶(非管理員)不能刪除用戶,不過我們還要確認管理員點擊刪除鏈接后能成功刪除用戶。因為刪除鏈接在用戶列表頁面,所以我們要把這個測試添加到用戶列表頁面的測試中([代碼清單 9.44](#listing-user-index-test))。這個測試唯一需要一點技巧的代碼是,管理員點擊刪除鏈接后如何確認用戶被刪除了。我們可以使用下面的代碼實現:
```
assert_difference 'User.count', -1 do
delete user_path(@other_user)
end
```
我們使用[代碼清單 7.26](chapter7.html#listing-a-test-for-valid-submission) 中檢查創建了一個用戶的 `assert_difference` 方法,不過這一次我們要確認向相應的地址發送 `DELETE` 請求后,`User.count` 的變化量是 `-1`,從而確認用戶被刪除了。
綜上所述,針對分頁和刪除操作的測試如[代碼清單 9.57](#listing-delete-link-integration-test) 所示,這段代碼既測試了管理員執行的刪除操作,也測試了非管理員執行的刪除操作。
##### 代碼清單 9.57:刪除鏈接和刪除用戶操作的集成測試 GREEN
test/integration/users_index_test.rb
```
require 'test_helper'
class UsersIndexTest < ActionDispatch::IntegrationTest
def setup
@admin = users(:michael)
@non_admin = users(:archer)
end
test "index as admin including pagination and delete links" do
log_in_as(@admin)
get users_path
assert_template 'users/index'
assert_select 'div.pagination'
first_page_of_users = User.paginate(page: 1)
first_page_of_users.each do |user|
assert_select 'a[href=?]', user_path(user), text: user.name
unless user == @admin
assert_select 'a[href=?]', user_path(user), text: 'delete',
method: :delete
end
end
assert_difference 'User.count', -1 do
delete user_path(@non_admin)
end
end
test "index as non-admin" do
log_in_as(@non_admin)
get users_path
assert_select 'a', text: 'delete', count: 0
end
end
```
注意,[代碼清單 9.57](#listing-delete-link-integration-test) 檢查了每個用戶旁都有刪除鏈接,而且如果用戶是管理員,就不做這個檢查(因為管理員旁不會顯示刪除鏈接,參見[代碼清單 9.52](#listing-delete-links))。
現在,刪除用戶的代碼有了良好的測試,而且測試組件應該能通過:
##### 代碼清單 9.58:**GREEN**
```
$ bundle exec rake test
```
- Ruby on Rails 教程
- 致中國讀者
- 序
- 致謝
- 作者譯者簡介
- 版權和代碼授權協議
- 第 1 章 從零開始,完成一次部署
- 1.1 簡介
- 1.2 搭建環境
- 1.3 第一個應用
- 1.4 使用 Git 做版本控制
- 1.5 部署
- 1.6 小結
- 1.7 練習
- 第 2 章 玩具應用
- 2.1 規劃應用
- 2.2 用戶資源
- 2.3 微博資源
- 2.4 小結
- 2.5 練習
- 第 3 章 基本靜態的頁面
- 3.1 創建演示應用
- 3.2 靜態頁面
- 3.3 開始測試
- 3.4 有點動態內容的頁面
- 3.5 小結
- 3.6 練習
- 3.7 高級測試技術
- 第 4 章 Rails 背后的 Ruby
- 4.1 導言
- 4.2 字符串和方法
- 4.3 其他數據類型
- 4.4 Ruby 類
- 4.5 小結
- 4.6 練習
- 第 5 章 完善布局
- 5.1 添加一些結構
- 5.2 Sass 和 Asset Pipeline
- 5.3 布局中的鏈接
- 5.4 用戶注冊:第一步
- 5.5 小結
- 5.6 練習
- 第 6 章 用戶模型
- 6.1 用戶模型
- 6.2 用戶數據驗證
- 6.3 添加安全密碼
- 6.4 小結
- 6.5 練習
- 第 7 章 注冊
- 7.1 顯示用戶的信息
- 7.2 注冊表單
- 7.3 注冊失敗
- 7.4 注冊成功
- 7.5 專業部署方案
- 7.6 小結
- 7.7 練習
- 第 8 章 登錄和退出
- 8.1 會話
- 8.2 登錄
- 8.3 退出
- 8.4 記住我
- 8.5 小結
- 8.6 練習
- 第 9 章 更新,顯示和刪除用戶
- 9.1 更新用戶
- 9.2 權限系統
- 9.3 列出所有用戶
- 9.4 刪除用戶
- 9.5 小結
- 9.6 練習
- 第 10 章 賬戶激活和密碼重設
- 10.1 賬戶激活
- 10.2 密碼重設
- 10.3 在生產環境中發送郵件
- 10.4 小結
- 10.5 練習
- 10.6 證明超時失效的比較算式
- 第 11 章 用戶的微博
- 11.1 微博模型
- 11.2 顯示微博
- 11.3 微博相關的操作
- 11.4 微博中的圖片
- 11.5 小結
- 11.6 練習
- 第 12 章 關注用戶
- 12.1 “關系”模型
- 12.2 關注用戶的網頁界面
- 12.3 動態流
- 12.4 小結
- 12.5 練習