Friendly forwarding
網站的權限系統已經完成,但還有個小問題,如果使用者嘗試瀏覽有權限的頁面,不管有沒有登入,都會被導向個人資料頁面。也就是說如果未登入的使用者想瀏覽編輯頁面,登入後會被導向到 /users/1,而不是 /users/1/edit,如果登入後可以導向到之前想瀏覽的頁面會更友善。
要實現這種應用程式碼會有點複雜,我們可以先從簡單的測試下手,也就是:
log_in_as(@user)
get edit_user_path(@user)
把先前的登入和瀏覽編輯頁面的順序顛倒,如以下程式碼所示:
test/integration/users_edit_test.rb
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
test "successful edit with friendly forwarding" do
get edit_user_path(@user)
log_in_as(@user)
assert_redirected_to edit_user_path(@user)
name = "Foo Bar"
email = "[email protected]"
patch user_path(@user), user: { name: name,
email: email,
password: "",
password_confirmation: "" }
assert_not flash.empty?
assert_redirected_to @user
@user.reload
assert_equal name, @user.name
assert_equal email, @user.email
end
end
先瀏覽編輯頁面,然後登入,最後檢查使用者是被導向到編輯頁面,而不是預設的個人資料頁面,同時也移除 assert_template 'users/edit' 這行。
現在有了失敗的測試,可以繼續實現友善的頁面導向。要讓使用者導向真正想瀏覽的頁面,我們要在某個地方儲存這個頁面的位址,登入後再轉向這個位址。所以要在 Sessions helper 定義一組方法:store_location 和 redirect_back_or:
app/helpers/sessions_helper.rb
module SessionsHelper
.
.
.
# Redirects to stored location (or to the default).
def redirect_back_or(default)
redirect_to(session[:forwarding_url] || default)
session.delete(:forwarding_url)
end
# Stores the URL trying to be accessed.
def store_location
session[:forwarding_url] = request.url if request.get?
end
end
我們使用 session 儲存位址,和之前定義使用者登入的方式類似。同時也使用 request 物件來取的請求頁面的 URL(透過 request.url)。
store_location 方法把請求的位址儲存在含有 key :forwarding_url 的 session 變數裡面,但只在 GET 請求中才儲存。這樣做的原因是,當未登入的使用者提交表單時,不會儲存轉向的位址(這種情況或許少見,但如果在提交表單之前,使用者手動刪除 session,還是會發生)。
而如果儲存了,那本來預期會接收 POST、PATCH 或 DELETE 請求的動作,實際接收到的卻是 GET,會導致錯誤。加上 if request.get? 能避免這種錯誤發生。
要使用 store_location 我們要把它加在 logged_in_user before filter 中:
app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
before_action :correct_user, only: [:edit, :update]
.
.
.
def edit
end
.
.
.
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
# Before filters
# Confirms a logged-in user.
def logged_in_user
unless logged_in?
store_location
flash[:danger] = "Please log in."
redirect_to login_url
end
end
# Confirms the correct user.
def correct_user
@user = User.find(params[:id])
redirect_to(root_url) unless current_user?(@user)
end
end
接著要實現轉向位址的話,我們要使用 redirect_back_or 方法,如果儲存了之前請求的位址,就重新導向到那個位址,否則就導向預設的位址:
session[:forwarding_url] || default
如果 session[:forwarding_url] 是 nil,就會執行 default。在 redirect_back_or 方法中還用了:
session.delete(:forwarding_url)
刪除了轉向的位址,因為如果不刪除,後續登入會不斷的導向到受保護的頁面,直到使用者關閉瀏覽器。(針對這個行為的測試留在練習題)
還要注意,即使先重新導向了,還是會刪除 session 中的轉向位址,除非使用明確的 return 或是到 method 的末端,否則重新導向之後,程式碼還是會繼續執行。
把 redirect_back_or 方法加在 Sessions controller 的 create action 裡面:
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
.
.
.
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_back_or user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
.
.
.
end
現在友善導向的測試應該能通過(Green):
$ bundle exec rake test