Laravel + Breeze + Inertia + Vue.js 環境のMulti Authentication
この記事について
個人的には、フロントエンドにVue.jsを利用することが増え、ちょっとしたウェブサービスを作成するのであれば、「Laravel + Breeze + Inertia + Vue.js」という構成が使い勝手がよいと感じています。
Breezeを利用することで、簡単に認証の仕組みは導入できるのですが、複数のロールごとに認証させたいということが、よくあります。この仕組みをMulti Authenticationと呼んでいますが、これを実現しようとすると、結構、泥くさい作業が必要です。
また、同様の記事も存在するのですが、状況によってそのまま動かないことも多いため、自身なりのアレンジで整理して手順をまとめてみます。
本記事は、Laravel中級者以上の読者を対象としており、入門的な部分については説明しておりませんので、ご了承ください。
なお、この記事においては、各ロールは互いに独立したロールを想定しており、権限管理の仕組みではありません。
動作確認環境
- PHP 8.1
- Laravel 10.10
手順の概要
なるべく個別のソースを書かないという方針に基づいていきたいと思っています。
そうすることにより、Laravel等のバージョンが異なっても方針自体は有効な内容となっているかと思います。
Multi Authentication環境の構築手順
Laravel + Inertia + Vue.js 環境の構築
まずは、通常通りに環境を構築していきます。
Laravel環境の構築
以下のオフィシャルのドキュメントに従って、Laravelの環境を構築します。
Macの場合であれば以下のターミナルにて以下のコマンドを実行することになります。
curl -s "https://laravel.build/<PROJECT_NAME>" | bash
cd <PROJECT_NAME>
./vendor/bin/sail up -d
Vue.jsを利用したBreeze環境の構築
こちらも以下のオフィシャルのドキュメントに従います。
まず、sailで構築されたアプリケーションのコンテナに入ります。
docker compose exec laravel.test bash
コンテナ内にて、Breezeの環境を構築します。
composer require laravel/breeze --dev
php artisan breeze:install vue
php artisan migrate
ここまでで、認証の仕組みとしてBreezeを利用し、フロントエンドにVue.js をした環境ができているはずです。
http://localhost にアクセスし、動作確認をしてみてください。
Breezeの認証の仕組みの概要
ここまでで、認証に関連する部分としては次のことが実現されています。
Laravelそのものによって準備されること
- app/Models/ に必要なModelを用意
- database/migrations/ に必要なmigrationファイルを用意
- config/auth.php に認証の設定ファイルを用意
- app/Http/Middleware/ に必要なMiddleware を用意
- Authenticate.php
- RedirectIfAuthenticated.php
Breezeによって準備されること
- routes/auth.php に認証関連のrouteを設定
- app/Http/Controllers/Auth 以下に、認証関連に必要なControllerを用意
- app/Http/Requests/Auth/LoginRequest.php にログインフォーム用のRequestを用意
- resources/js/Pages/Auth/ 以下に認証関連のページを用意
- resources/js/Layouts/AuthenticatedLayout.vue として、ログイン後のレイアウトを用意
詳細な説明はしませんが、Laravelが提供するものは認証基盤です。標準的には、usersテーブルに格納されたデータを使い、Userモデルによって認証されるようになっています。
また、Breezeが提供するものは、その基盤に基づいたControllerや画面の定義といったWebシステムとして表示する仕組みになります。
認証基盤のMulti Authentication化
ロールの定義
今回は、Userと同様の認証を複数ロールで実現できることを考えます。
以下では、memberとadminという2種類のロールで認証することを例にして説明します。
この2つのロールの認証のために、ModelとMigrationファイルを用意します。
php artisan make:model -m Member
php artisan make:model -m Admin
これで、以下のファイルが作成されます。
- app/Models/Member.php
- app/Models/Admin.php
- database/migrations/YYYY_MM_dd_HHmmss_create_members_table.php
- database/migrations/YYYY_MM_dd_HHmmss_create_admins_table.php
Member.phpとAdmin.phpは、それぞれ、Memberモデル、Adminモデルの実装になりますが、この時点では、既に作られているUserモデルのソースをコピーし、クラス名だけ変更すれば大丈夫です。
Migrationファイルも同様で、2014_10_12_000000_create_users_table.phpのupの中の定義と同様な要素を記述すればよいです。
参考までに、手元で実施したバージョンでと、Memberの方はこんな感じになりました。
app/Models/Member.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class Member extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
databaase/migrations/YYYY_MM_dd_HHmmss_create_members_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('members', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('members');
}
};
認証におけるロールの利用
さて、Laravelの認証の仕組みにおいて、どのロールにおいての認証であるかは、guard として表現するのが自然です。その辺りの設定は、config/auth.phpの中に記述します。このファイルでは、1つの連想配列を定義する形になっているのですが、そのうち、gurads、providers、passwordsの要素に、ロールごとの情報を追記します。
config/auth.php
... 省略 ...
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'member' => [
'driver' => 'session',
'provider' => 'members',
],
'admin' => [
'driver' => 'session',
'provider' => 'admins',
],
''
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
'members' => [
'driver' => 'eloquent',
'model' => App\Models\Member::class,
],
'admins' => [
'driver' => 'eloquent',
'model' => App\Models\Admin::class,
],
],
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_reset_tokens',
'expire' => 60,
'throttle' => 60,
],
'members' => [
'provider' => 'members',
'table' => 'password_reset_tokens',
'expire' => 60,
'throttle' => 60,
],
'admins' => [
'provider' => 'admins',
'table' => 'password_reset_tokens',
'expire' => 60,
'throttle' => 60,
],
],
... 省略 ...
簡単に記述の意味を説明しておきます。
guardsの要素として以下の指定があるため、memberというguardは、membersというproviderで提供される(定義される)という宣言となります。
'member' => [
'driver' => 'session',
'provider' => 'members',
],
次にprovidersの要素の中に、以下の指定があるため、membersというproviderは、App\Models\MemberというEloquentモデルを使ったもので実現されることを指定しています。
'members' => [
'driver' => 'eloquent',
'model' => App\Models\Member::class,
],
同様にpasswordsの要素で認証に関する指定を行なっています。
URLの設計
次に各ロールとしての機能を提供するURLについて考えます。
ここでは、各ロールは互いに独立なものと想定していますので、
http://exmaple.com/members/ から始まるURLでmember向けの機能を提供し、
http://exmaple.com/admins/ から始まるURLでmember向けの機能を提供することとします。
そうすると、現在のURLから、どのロールとしてのアクセスかを判別できることとなります。
そのため、以下のようなヘルパー関数を用意しておきます。
app/detect_role.php
<?php
use Illuminate\Support\Str;
if (!function_exists('detect_role')) {
function detect_role(): ?string
{
$current_uri = request()->path();
$roles = ['member', 'admin'];
foreach ($roles as $role) {
if (Str::startsWith($current_uri, $role)) {
return $role;
}
}
return null;
}
}
また、この関数を色々なところで使えるように、composer.jsonにも追記しておきます。
composer.json
{
... 省略 ...
"autoload": {
"files": [
"app/detect_role.php"
],
... 省略 ...
},
... 省略 ...
}
ここまでで事前準備が完了です。
WebシステムとしてのMulti Authentication化
ルーティングの設定
Breezeが用意した初期状態では、routes/web.phpは、以下のようになっています。
Route::get('/', function () {
return Inertia::render('Welcome', [
'canLogin' => Route::has('login'),
'canRegister' => Route::has('register'),
'laravelVersion' => Application::VERSION,
'phpVersion' => PHP_VERSION,
]);
});
Route::get('/dashboard', function () {
return Inertia::render('Dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});
require __DIR__.'/auth.php';
このファイルの内容は以下のようになっています。
- 未ログイン状態のアクセス (最初のブロック)
- ログイン後に提供する機能に関するルーティング (続くブロック)
- 認証関連のルーティング (requireで取り込んでいる部分)
このままmemberとしての認証とか、memberとしてログイン済みとかを記述すると煩雑になってしまうため、以下のような構造にします。
routes/web.php
Route::get('/', function () {
return Inertia::render('Welcome', [
'canLogin' => Route::has('login'),
'canRegister' => Route::has('register'),
'laravelVersion' => Application::VERSION,
'phpVersion' => PHP_VERSION,
]);
});
// memberとしての認証、および、機能のルーティング
require __DIR__ . '/member.php';
// adminとしての認証、および、機能のルーティング
require __DIR__ . '/admin.php';
その上で、各ロールごとのweb.phpは以下のようになります。以下では、member.phpを示しますが、admin.phpも同様です。
routes/member.php
<?php
use App\Http\Controllers\Member\ProfileController;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::prefix('member')->name('member.')->group(function () {
Route::middleware(['auth:member'])->group(function() {
Route::get('/dashboard', function () {
return Inertia::render('Member/Dashboard');
})->name('dashboard');
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});
require __DIR__ . '/auth.php';
});
大きくRoute::prefixで囲うことにより、/member以下のURLのときのルーティングであることを示し、その内側の Route::middleware(['auth:member'])によって、memberとしてログインしている際のルーティングを定義しています。
また、ルーティングの名前も、元々がdashboardだったものは、member.dashboard、admin.dashboardとなり、auth.phpの中のルーティングに関しても、loginだったものが、member.login、admin.loginといった名前で定義されることとなります。
なお、初期状態に合わせて、ダッシュボードと、プロフィールの操作についてのルーティングを記載しています。ダッシュボードの表示するページのリソースがMember/Dashboardとなっており、ProfileControllerがMemberパッケージの下のものの指定になっていることにご注意ください。これらに対応するファイルはこの後で作成します。
本当はこの時点でルーティングを確認したいのですが、まだ作成していないクラスなどがあり、この時点でルーティングを確認しようとするとエラーとなってしまいますので、もう少し、お待ちください。
※このauth:member というところにguard(ロール)を指定するのを忘れると、その指定が異なるということに気付きにくいエラーとなるので、注意してください。
認証関連のControllerの設定
ロールに関わらず認証に関係するControllerは、App\Http\Controllers\Authのものを共通で利用することとします。共通部分をまとめるために、app/Http/Controllers/Auth/AuthController.phpに、共通の親クラスを用意します。この後、必要に応じてヘルパーメソッドを追加していきますが、まずは現在のロールをコンストラクタで記憶しておくようにします。
app/Http/Controllers/Auth/AuthController.php
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Support\Str;
class AuthController extends Controller
{
private ?string $role;
private string $package;
public function __construct() {
$this->role = detect_role();
if (empty($this->role)) {
$this->package = '';
} else {
$camel = Str::camel($this->role);
$camel[0] = strtoupper($camel[0]);
$this->package =$camel;
}
}
}
次に、Auth以下の各Controllerを全ロールで共通で利用できるように修正していきます。
修正が必要な箇所は、以下の5つです。
- HOMEの設定
- ルーティングの設定
- レンダリング対象の設定
- モデルクラスの変更
- Requestからuserオブジェクトの取得
まず、HOMEの設定です。もともとの実装だと、app/Providers/RouteServiceProvider.phpで定義されたRouteServiceProviderのHOMEというstatic変数で制御するようになっています。このままだと、どのロールでログインしてもこのアドレスにリダイレクトされるようになってしまいます。
そこで、以下のようにstaticなhome関数を導入します。また、念のため、HOMEをprivateに変更しています。
app/Providers/RouteServiceProvider.php
class RouteServiceProvider extends ServiceProvider
{
... 省略 ...
private const HOME = '/dashboard';
public static function home() {
$role = detect_role();
return empty($role) ? self::HOME : '/' . $role . self::HOME;
}
... 省略 ...
その上で、例えば、AuthenticatedSessionControllerのstoreメソッドを以下のように書き変えます。
変更前
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(RouteServiceProvider::HOME);
}
変更後
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(RouteServiceProvider::home());
}
次に、ルーティングの設定です。
こちらも、loginという名前で参照しているルーティングが利用されていますが、このままではどのロールのログインかが分かりません。既にルーティングの方では、member.login、admin.loginと名前を設定しています。
この名前の変換を行なえるように、AuthControllerに、routeメソッドを定義します。このメソッドでは、ロールに応じてルーティングの名前を変換するようにしています。
app/Http/Controllers/Auth/AuthController.php
<?php
... 省略 ...
class AuthController extends Controller
{
... 省略 ...
public function route($route): string
{
if (empty($this->role)) {
return $route;
}
return $this->role . '.' . $route;
}
... 省略 ...
}
例えば、NewPasswordControllerのstoreメソッドは、routeメソッドを利用して、以下のように書き変えます。
変更前
public function store(Request $request): RedirectResponse
{
... 省略 ...
if ($status == Password::PASSWORD_RESET) {
return redirect()->route('login')->with('status', __($status));
}
... 省略 ...
}
変更後
public function store(Request $request): RedirectResponse
{
... 省略 ...
if ($status == Password::PASSWORD_RESET) {
return redirect()->route($this->route('login'))->with('status', __($status));
}
... 省略 ...
}
次にレンダリングするリソース名の対応をします。
もともとは Auth/Login.vue で定義されたページをレンダリングするような記述になっています。
ロールによって表示内容は変更すべきかと思いますので、Member/Auth/Login、Admin/Auth/Loginなどと使い分けたいです。
そのために、AuthControllerに、resourceメソッドを定義します。このメソッドを使うことで、ロールに応じたリソース名に変換できるようになります。
app/Http/Controllers/Auth/AuthController.php
<?php
... 省略 ...
class AuthController extends Controller
{
... 省略 ...
public function resource($path): string
{
if (empty($this->role)) {
return $path;
}
return $this->package . '/' . $path;
}
... 省略 ...
}
例えば、AuthenticatedSessionControllerクラスのcreateメソッドは以下のように書き変えます。
変更前
public function create(): Response
{
return Inertia::render('Auth/Login', [
'canResetPassword' => Route::has($this->route('password.request')),
'status' => session('status'),
]);
}
変更後
public function create(): Response
{
return Inertia::render($this->resource('Auth/Login'), [
'canResetPassword' => Route::has($this->route('password.request')),
'status' => session('status'),
]);
}
最後に、モデルクラスの名前に対する調整を行ないます。
RegisteredUserControllerのstoreメソッドでは、Userモデルのクラスが明示的に指定してあり、このままだと、どんなロールであってもUserモデルが使われてしまいます。
そこでロールに応じたモデルクラスの名前を取得するためのmodelNameメソッドをAuthControllerに実装します。
app/Http/Controllers/Auth/AuthController.php
<?php
... 省略 ...
class AuthController extends Controller
{
... 省略 ...
public function modelName() {
if (empty($this->role)) {
return User::class;
}
return '\\App\\Models\\' . $this->package;
}
... 省略 ...
}
このメソッドを利用することで、RegisteredUserControllerのstoreメソッドは以下のように書き変えることができます。
変更前
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
変更後
$modelName = $this->modelName();
$user = $modelName::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
最後に、認証関連のControllerの中には、
$request->user()
という表現が何度か出てきます。
実は、このuserメソッドにはguardを引数として指定する必要がありますので、このままだと期待通りの動作となります。
このためのヘルパーを用意してあげます。
ここまでのヘルパーは認証関連に特化していたので。AuthControllerに実装していましたが、このヘルパーはもう少し広い範囲で利用されそうなので、Controllerに実装することとします。
app/Http/Controllers/Controller.php
<?php
namespace App\Http\Controllers;
... 省略 ...
class Controller extends BaseController
{
... 省略 ...
public function user($request) {
$role = detect_role();
return $request->user($role);
}
... 省略 ...
}
その上で、
$request->user()
を、
$this->user($request)
と置き変えます。
例えば、PasswordControllerのupdateメソッドは以下のように書き変えます。
public function update(Request $request): RedirectResponse
{
... 省略 ...
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
... 省略 ...
}
public function update(Request $request): RedirectResponse
{
... 省略 ...
$this->user($request)->update([
'password' => Hash::make($validated['password']),
]);
... 省略 ...
}
その他のControllerの設定
また、各ロールのProfileControllerが未定義になっているので、それぞれロールごとに名前空間を掘って以下のファイルを作成します。
- app/Http/Controllers/Member/ProfileController.php
- app/Http/Controllers/Admin/ProfileController.php
これらのファイルも共通化を図ってもよいのですが、個別の用途がある、あるいは、特定のロールでは不要といったことも多いかと思うので、敢えて共通化を図らず、それぞれ作成しています。
Authの中のControllerと同様に、ProfileControllerに対しても以下の修正する必要がありますが、こちらのControllerはロールごとに実装があるので、個別に修正して構いません。Requestからuserオブジェクトを取得する部分に関しては、Controllerに定義したuser()メソッドを使った方が見通しはよいと思います。
- HOMEの設定
- ルーティングの設定
- レンダリング対象の設定
- モデルクラスの変更
- Requestからuserオブジェクトの取得
Middlewareの設定
Middlewareの実装の一部もMulti Authenticationのために修正が必要ですが、修正箇所は同様です。
手元の環境では以下の修正が必要でした。
AuthenticateクラスのredirectToメソッド
変更前
... 省略 ...
class Authenticate extends Middleware
{
... 省略 ...
protected function redirectTo(Request $request): ?string
{
return $request->expectsJson() ? null : route('login');
}
... 省略 ...
}
変更後
... 省略 ...
class Authenticate extends Middleware
{
... 省略 ...
protected function redirectTo(Request $request): ?string
{
$role = detect_role();
$route = empty($role) ? 'login' : $role . '.login';
return $request->expectsJson() ? null : route($route);
}
... 省略 ...
}
HandleInertiaRequestsクラスのshareメソッド
変更前
... 省略 ...
class HandleInertiaRequests extends Middleware
{
... 省略 ...
public function share(Request $request): array
{
$role = detect_role();
return array_merge(parent::share($request), [
'auth' => [
'user' => $request->user(),
],
... 省略 ...
]);
}
... 省略 ...
}
変更後
... 省略 ...
class HandleInertiaRequests extends Middleware
{
... 省略 ...
public function share(Request $request): array
{
$role = detect_role();
return array_merge(parent::share($request), [
'auth' => [
'user' => $request->user($role),
],
... 省略 ...
]);
}
... 省略 ...
}
RedirectIfAuthenticatedクラスのhandleメソッド
変更前
... 省略 ...
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);
}
}
return $next($request);
}
... 省略 ...
}
変更後
... 省略 ...
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());
}
}
return $next($request);
}
... 省略 ...
}
Requestの設定
認証関連でLoginRequestが提供されています。
こちらもMulti Authenticationに対応させる必要があります。
具体的な変更としては、ここまでのものとは異なるのですが、仕組みとしては同様です。
パスワードの認証にAuth::attemptを利用していたのですが、このstaticメソッドでは、Userモデルを利用して認証しようとします。そのため、この部分を、auth($guard)->attempに置き変えてあげる必要があります。
具体的には以下のように変更します。
変更前
class LoginRequest extends FormRequest
{
... 省略 ...
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
$role = detect_role();
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
... 省略 ...
}
RateLimiter::clear($this->throttleKey());
}
... 省略 ...
}
変更後
class LoginRequest extends FormRequest
{
... 省略 ...
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
$role = detect_role();
if (! auth($role)->attempt($this->only('email', 'password'), $this->boolean('remember'))) {
... 省略 ...
}
RateLimiter::clear($this->throttleKey());
}
... 省略 ...
}
画面の作成
最後、画面関連のファイルを対応します。
認証関連の画面のファイルは、resources/js/Pages/Authの中に入っています。
画面回りも共通化するよりもロールごとに分けた方が管理がしやすいと思うので、Pagesの下にMember、Adminというディレクトリを作成し、その下にAuthディレクトリをコピーします。
同様に、Dashboardも各ロールのディレクトリにコピーします。
Layoutも個別に用意した方が都合がよいことが多いと思うので、AuthenticatedLayout.vueをコピーして、MemberLayout.vue、AdminLayout.vueを作成します。
その上で、Dashboard.vueで利用しているLayoutを、それぞれ対応するLayoutに変更します。
以上で必要なファイルは一通り揃ったことになりますが、vueのファイルの中のルーティングも適切に修正する必要があります。これらのファイルもロールごとにあるので共通化は考えずに修正して構いません。
一例として、member用のDashboardは以下のようになります。
resources/js/Pages/Member/Dashboard.vue
<script setup>
import MemberLayout from '@/Layouts/MemberLayout.vue';
import { Head } from '@inertiajs/vue3';
</script>
<template>
<Head title="Dashboard" />
<MemberLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Dashboard</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 text-gray-900">You're logged in!</div>
</div>
</div>
</div>
</MemberLayout>
</template>
動作確認
ここまで対応すると、次のようにして動作確認ができます。
php artisan migrate
npm run dev
この状態で、http://localhost/member/register にアクセスすると、memberとして登録する画面が表示され、登録が完了すると、http://localhost/member/dashboard に移動することができるはずです。
さいごに
一通り作成したものを以下のリポジトリに置いてありますので、記事内で説明できていない部分などは適宜参照してください。
似たようなことはよく実施するので、ツール化しようかとも何度か思うのですが、memberは自由に登録できるが、adminはそのような機能は不要などといった個別対応も多く、汎用的なツールにしにくく、まだ泥臭く対応しています。
Breezeで作った雛形に対して、記事中の5点に注意して書き換えれば、恐らくある程度、汎用的には対応できるのではないかと思います。
もっとよい方法などあれば、お知らせください。
なお、detect_roleの考え方などは、以下を参考にさせて頂きました。ありがとうございます。
Discussion