Laravelで簡易的な管理者機能を作る
目標
LaravelでAPIサーバーを作り、アカウント登録・ログイン・ログアウトなどのユーザー認証機能、管理者によるアカウントBAN、削除機能を構築する。
リポジトリ
↓
今回のリポジトリ
主な機能
- ユーザーアカウント登録・Eメール認証
- ログイン
- ログアウト
- アカウント情報取得
- 通知一覧の取得
- 通知の既読
- 管理者UIへのアクセス・アクセス制限
- ユーザーアカウントの削除・停止
※バックエンドのみ記事に書くので、UI部分はレポジトリを参照してください。
環境構築
- 使用フレームワーク・ライブラリ・パッケージ
- Next.js
- Laravel
- spatie/laravel-permission
- larave/fortify
- axios
- @radix-ui/themes
事前準備
- データベース接続
今回はSQLiteを使って接続します。
Laravel Fortifyの設定
Laravel Fortifyは、Laravelのフロントエンドにとらわれない認証バックエンドの実装です123。Fortifyは、ログイン、ユーザー登録、パスワードのリセット、メールの検証など、Laravelの認証機能をすべて実装するために必要なルートとコントローラを登録します
公式ドキュメントより
主な機能
- ログイン機能(Rate Limiting機能付き)
- ユーザー登録機能
- パスワードリセット機能
- メール認証機能
- プロフィール情報の更新機能
- パスワードの更新機能
- 2段階認証機能
バックエンドのみ機能が提供される。オプションを設定すれば、viewも表示される
インストール
composer require laravel/fortify
サービスプロバイダへの登録
config/app.php
'providers' => [
// 略
App\Providers\FortifyServiceProvider::class,
]
コマンド実行
php artisan migrate
configファイルの設定
今回はバックエンドで使用する想定なので、以下のように設定ファイルを編集します
config/fortify.php
// ルーティングのprefix
'prefix' => 'api',
// ビューのルーティングを無効化
'views' => false,
// 各認証機能
'features' => [
Features::registration(),
Features::resetPasswords(),
Features::emailVerification(),
// Features::updateProfileInformation(),
// Features::updatePasswords(),
// Features::twoFactorAuthentication([
// 'confirm' => true,
// 'confirmPassword' => true,
// 'window' => 0,
// ]),
]
モデルの変更
もしEメール認証機能を使用する場合、Userモデルを編集しないと確認メールが届きません。
メールの確認が必要ない場合、編集の必要はありません。
app/models/user.php
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements MustVerifyEmail {
// ~
}
Laravel Permissionの設定
UserとRoleを紐づけるための中間テーブルを管理するライブラリ
RBAC方式のアクセス制御を実現する
AWSのIAMのイメージ
- ユーザーに役割を当てる
- ユーザーが特定の役割を持っているか確認
- 役割に基づいたアクセス制御
- 権限の管理
今回は簡易的ということで、2つのroleを設定します。通常ユーザーはroleを付与しません
role | 説明 |
---|---|
admin | 管理者 |
banned | アカウント停止ユーザー |
インストール
composer require spatie/laravel-permission
サービスプロバイダへの登録
自動で登録されていない場合、記述してください
config/app.php
'providers' => [
// 略
Spatie\Permission\PermissionServiceProvider::class,
];
Kernel.phpへの登録
Kernel.php
protected $routeMiddleware = [
'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
];
コマンド実行
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider
php artisan config:clear
php artisan migrate
モデルの変更
roleをUserモデルに紐づけます
App/Models/User.php
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use HasRoles; // 追加
// ...
}
Laravel本体に関する設定
認証状態の場合のレスポンスをJSONに変更
ログイン状態の時、HomeのURIリダイレクトしてしまうので、JSONレスポンスが返ってくるように編集します。
RedirectIfAuthenticated.php
use Illuminate\Http\JsonResponse;
class RedirectIfAuthenticated
{
public function handle(Request $request, Closure $next, string ...$guards): Response
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
// return redirect(RouteServiceProvider::HOME);
$is_banned = User::find(Auth::id())->getRoleNames()->contains('banned');
return response()->json([
'login' => true,
'is_banned' => $is_banned,
], 200);
}
}
return $next($request);
}
}
FortifyServiceProvider.php
registerメソッドを以下のように編集します
public function register(): void
{
$this->app->instance(LoginResponse::class, new class implements LoginResponse
{
public function toResponse($request)
{
$is_banned = Auth::user()->getRoleNames()->contains('banned');
if ($is_banned) {
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return response()->json([
'is_banned' => $is_banned,
], 403);
}
return response()->json([
'login' => true,
'is_banned' => $is_banned,
]);
}
});
$this->app->instance(LogoutResponse::class, new class implements LogoutResponse
{
public function toResponse($request)
{
return response()->json([
'code' => 200,
'user' => $request->user(),
]);
}
});
}
通知クラスの作成
Webサービスには運営からの通知を受け取る機能がよくあります。なのでそれを疑似的に作ろうと思います。
Middlewareの作成
ユーザーがアカウント停止状態の時、403エラーとJSONレスポンスを返すmiddleware "CheckBanned"を作成する
public function handle(Request $request, Closure $next): Response
{
if (Auth::check()) {
if (Auth::user()->getRoleNames()->contains('banned')) {
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return response()->json([
'banned' => true,
], 403);
} else {
return $next($request);
}
}
return response()->json([
'login' => false,
], 401);
}
Middlewareの登録
kernel.php
protected $middlewareAliases = [
'is_banned' => \App\Http\Middleware\CheckBanned::class,
];
Seederファイルの編集
DatabaseSeeder.php
<?php
namespace Database\Seeders;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use App\Notifications\TestNotification;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
$i = 1;
// \App\Models\User::factory(10)->create();
\App\Models\User::factory()->create([
'name' => 'test',
'email' => 'test@test.com',
'password' => 'password',
]);
\App\Models\User::factory()->create([
'name' => 'test1',
'email' => 'test1@test.com',
'password' => 'password',
]);
\App\Models\User::factory(2)->create();
$user = \App\Models\User::find(1);
while ($i <= 20) {
$user->notify(new TestNotification(\App\Models\User::find(2)));
$i++;
}
$admin = Role::create(['name' => 'admin']);
$permissionOfDeleteAccount = Permission::create(['name' => 'delete account']);
$admin->givePermissionTo($permissionOfDeleteAccount);
$user->assignRole('admin');
$banned = Role::create(['name' => 'banned']);
}
}
APIルートの定義
api.php
を編集します。
<?php
use Illuminate\Http\Request;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
Route::middleware(['auth:sanctum', 'is_banned'])->get('/user', function (Request $request) {
return response()->json([
'user' => auth()->user(),
'notifications' => $request->user()->unreadNotifications,
'role' => auth()->user()->getRoleNames(),
]);
});
Route::middleware('role:admin')->get('/users', function (Request $request) {
$data = \App\Models\User::where('id', '!=', Auth::id())->get();
$users = [];
foreach ($data as $user) {
array_push($users, [
'user' => $user,
'role' => \App\Models\User::find($user->id)->getRoleNames(),
]);
}
return response()->json($users);
});
Route::middleware('role:admin')->post('/users/{id}/ban', function (Request $request) {
$user = \App\Models\User::find($request->id);
$user->assignRole('banned');
return response()->json([
'banned' => true,
]);
});
Route::middleware('role:admin')->delete('/users/{id}', function (Request $request) {
\App\Models\User::where('id', $request->id)->delete();
return response()->json([
'delete' => true,
]);
});
Route::post('/notifications/{notification}/read', function (DatabaseNotification $notification) {
$notification->markAsRead();
return response()->json([
'read' => true,
]);
});
Route::post('/notifications/read-all', function (Request $request) {
auth()->user()->unreadNotifications->markAsRead();
return response()->json([
'read' => true,
]);
});
UI
詳しくはgithubのリポジトリを参照してください。
各画面の写真
-
トップ画面(未ログイン時)
-
ログイン画面
-
ログイン画面(アカウント停止状態のユーザーがログインした場合)
-
サインアップ画面
-
確認メール
-
ログイン後のトップ画面(adminロール)
-
ログイン後のトップ画面(adminではないユーザー)
-
通知ドロップダウン
-
通知の詳細情報のダイアログ
-
通知の一括既読したあと
-
adminダッシュボード
-
adminダッシュボード(ユーザーをアカウント停止状態にした後)
参考
各公式ドキュメント
Laravel Permissionを使用して簡単に権限を付与する
Laravel FortifyのレスポンスをJSONで返す方法
エラーが発生したら、コメントで書いてください。
Discussion