原本我們是想把一些路由和頁面流程先建立起來的,但是在改完後台時,發現目前的角色登入後的畫面都在同一個 backstage,我們希望將來能區分管理者
和一般使用者
的差異,不同的角色看到的後台應該是不一樣的。
當然,不一樣的後台又會衍生出不一樣的功能需求及頁面的內容,所以我們如果想把整個系統的流程定下來,要先解決這個角色的問題才行。
我們先簡單的在 users 這張資料表上加一個 role 的欄位,之後我們再根據這個角色的欄位來決定使用者可以做那些事,去那些畫面就可以了。
使用 migration 來替原本的資料表加上欄位:
php artisan make:migration add_role_column_to_users --table=users
檔名清楚一點,有助於未來的維護和追踪
撰寫migration時,記得把down的內容也補上,這樣在做rollback時,才會有正確的結果
.....略
return new class extends Migration
{
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('role')->default('user');
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('role');
});
}
};
然後執行 migrate 來增加欄位
php artisan migrate
我們現在可以知道使用者登入的角色的了,預設都是一般 user
,可想而知,未來需要再做一個功能讓管理者可以調整使用者的角色,這個部份,三十天後我們再做一篇專文來說明,現在我們先手工直接在資料表上改就好。
先註冊第二個使用者,並直接手動更改其中一個使用者的角色為 admin。
由於 Breeze
已經幫我們做好了一套使用者的驗證機制,所以所有要到後台的路由都會經由這個 auth 的中介層來控管,因此我們要從這邊下手,而不是只改登入而已,我們希望所有關於認證後的導向動作除了原本的判斷是否為登入使用者外,還要多判斷一個使用者角色。
移動到 App\Http\Middleware\RedirectIfAuthenticated.php,這個中介層使用在登入和重設密碼相關的動作上,影響到的路由可以參考 routes\auth.php,我們來調整一下這個內容:
.....略
class RedirectIfAuthenticated
{
(\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @param string|null ...$guards
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next, ...$guards)
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
//根據不同的角色,導向到不同的頁面去
switch(Auth::user()->role){
case 'user':
return redirect(RouteServiceProvider::USERHOME);
break;
case 'admin':
return redirect(RouteServiceProvider::HOME);
break;
}
}
}
return $next($request);
}
}
因為我們是打算用 RouteServiceProvider 來統一導向的路由,所以我們同時得改一下這個 Provider 的設定,增加一個常數來使用:
app\Providers\RouteServiceProvider.php
public const HOME = '/backstage';
public const USERHOME = '/userhome';
如果時間允許,我會把所有的HOME都改成BACKSTAGE,這樣才能統一語意,日後好維護,不過現在先暫時用內建的就好。
登入的導向也得改一下:
app\Http\Controllers\Auth\AuthenticatedSessionController.php
public function store(LoginRequest $request)
{
$request->authenticate();
$request->session()->regenerate();
switch(Auth::user()->role){
case 'user':
return redirect()->intended(RouteServiceProvider::USERHOME);
break;
case 'admin':
return redirect()->intended(RouteServiceProvider::HOME);
break;
}
}
建立一個 UserController 用來管理使用者的行為,包括會員中心:
php artisan make:controller UserController
app\Http\Controllers\UserController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Inertia\Inertia;
class UserController extends Controller
{
function userCenter(){
return Inertia::render('UserHome');
}
}
補上 userhome 的路由及頁面
routes\web.php
Route::get('/backstage', [BackstageController::class,'controlPanel'])
->middleware(['auth', 'verified'])
->name('backstage');
Route::get('/userhome', [BackstageController::class,'userCenter'])
->middleware(['auth', 'verified'])
->name('userhome');
複製resources\js\Layouts\AuthenticatedLayout.vue做為使用者中心的版型檔案,所有的路由名稱都要跟著改一下
resources\js\Layouts\AuthenticatedUserLayout.vue
<template>
.....略
<nav class="bg-white border-b border-gray-100">
<!-- Primary Navigation Menu -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<!-- Logo -->
<div class="shrink-0 flex items-center">
<!--路由連結修改-->
<Link :href="route('userhome')">
<ApplicationLogo class="block h-9 w-auto" />
</Link>
</div>
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
<!--路由連結修改-->
<NavLink :href="route('userhome')"
:active="route().current('userhome')">
會員中心
</NavLink>
</div>
</div>
.....略
</div>
</div>
<!-- Responsive Navigation Menu -->
<div :class="{'block': showingNavigationDropdown,
'hidden': ! showingNavigationDropdown}"
class="sm:hidden">
<div class="pt-2 pb-3 space-y-1">
<!--路由連結修改-->
<ResponsiveNavLink :href="route('userhome')"
:active="route().current('userhome')">
會員中心
</ResponsiveNavLink>
</div>
.....略
</div>
</nav>
.....略
</template>
當然,未來可以想個方法,簡化不同角色的路由導向方式,就不用額外增加這麼多組件了。
接著增加一個 UserHome.vue 的檔案,以剛才的 AuthenticatedUserLayout 組件做為版型,建立會員中心的內容頁面:
<script setup>
import AuthenticatedUserLayout from "@/Layouts/AuthenticatedUserLayout.vue";
import { Head } from "@inertiajs/inertia-vue3";
</script>
<template>
<Head title="管理中心" />
<AuthenticatedUserLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
會員中心
</h2>
</template>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
<ul>
<li>可以查看自己測驗成績</li>
<li>可以對做過的題目進行註解</li>
<li>可以標記題目,方便日後查詢</li>
</ul>
</div>
</div>
</div>
</div>
</AuthenticatedUserLayout>
</template>
最後,我們使用不同角色的帳號登入,應該會被導到不同的後台頁面去:
使用者後台
管理者後台
不過還有個問題沒解決,就是如果使用者登入後,直接把網址改成 /backstage 的話,那他是可以直接看到管理中心的畫面的,這是因為預設只有做到帳號的認證,沒有在看角色的,所以我們自己做一個中介層,放在路由檢查的最後面,檢查一下請求者的身份和路由是否一致:
php artisan make:middleware RedirectByAuthcatedRole
app\Http\Middleware\RedirectByAuthcatedRole.php
.....略
class RedirectByAuthcatedRole
{
public function handle(Request $request, Closure $next)
{
//建立一個路由和角色的對應陣列
$authUri=['user'=>'/userhome','admin'=>'/backstage'];
//判斷使用者是否是此路由的合法使用者
if($request->getRequestUri()!=$authUri[Auth::user()->role]){
switch(Auth::user()->role){
case 'user':
return redirect(RouteServiceProvider::USERHOME);
break;
case 'admin':
return redirect(RouteServiceProvider::HOME);
break;
}
}
return $next($request);
}
}
記得註冊一下自己的 middleware:
app\Http\Kernel.php
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'auth.role' => \App\Http\Middleware\RedirectByAuthcatedRole::class,
];
把 middleware 加入到路由中:
routes\web.php
Route::get('/backstage', [BackstageController::class,'controlPanel'])
->middleware(['auth', 'verified','auth.role'])
->name('backstage');
Route::get('/userhome', [UserController::class,'userCenter'])
->middleware(['auth', 'verified','auth.role'])
->name('userhome');
都做到這田地了,前台首頁在登入後會在右上方出現一個dashboard連結,預設是導向原本的管理者後台,我們也來改一下好了,讓不同的登入者顯示他的名字和後台連結:
app\Http\Controllers\HomeController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Auth;
class HomeController extends Controller
{
function home() {
return Inertia::render('Home', [
'canLogin' => Route::has('login'),
'canRegister' => Route::has('register'),
'username'=>Auth::user()->name??'訪客',
'role'=>Auth::user()->role??'guest',
]);
}
}
resources\js\Pages\Home.vue
<script setup>
import { Head, Link } from "@inertiajs/inertia-vue3";
import { reactive } from "vue";
const props = defineProps({
canLogin: Boolean,
canRegister: Boolean,
username: String,
role: String,
});
const userInfo = reactive({
user: {
link: route("userhome"),
string: "會員中心",
},
admin: {
link: route("backstage"),
string: "管理中心",
},
});
</script>
<template>
<Head title="學科測驗系統" />
<div
class="relative flex items-top justify-center min-h-screen
bg-gray-100 dark:bg-gray-900 sm:items-center sm:pt-0">
<div v-if="canLogin" class="hidden fixed top-0 right-0 px-6 py-4 sm:block">
{{ username }}
<Link v-if="$page.props.auth.user"
:href="userInfo[role].link"
class="text-sm text-gray-700 dark:text-gray-500 underline">
{{ userInfo[role].string }}
</Link>
<template v-else>
<Link :href="route('login')"
class="text-sm text-gray-700 dark:text-gray-500 underline">
Log in
</Link>
<Link v-if="canRegister"
:href="route('register')"
class="ml-4 text-sm text-gray-700 dark:text-gray-500 underline">
Register
</Link>
</template>
</div>
<!--前台主要內容區域-->
<div>
<ul>
<li>可以選擇學科別</li>
<li>可以選擇測驗或是練習</li>
<li>可以單純查看題庫</li>
</ul>
</div>
</div>
</template>
訪客頁面
使用者頁面
管理者頁面
不過這裏有留下一個問題,因為我路由字串判斷是寫死的,但後台頁面的路由可能很多,如果每個不同頁面的路由都要做檢查,那判斷式不就會很邏嗦?不過這個問題有點麻煩,我還在思考是要用 Policy
來做授權管理,還是直接在 middleware
寫死一個路由表來直接處理掉?
真實的開發就是這樣,一邊做,問題一邊跑出來,這個問題我們先記在牆上,晚點再來處理,先知道 middleware 可以用來做請求的過濾就可以了。
在今天的工作中,我們讓 User多了一個角色的欄位,並且讓每個User在登入時的可以依照角色的不同而到不同的頁面去。
那麼,今天就先這樣了。