この記事について
LaravelでJetstreamを使ったマルチログインの実装方法を覚書きとしてまとめます。
基本的には参考にさせていただいた記事と同様ですが、以下の点が異なります。
- LoginControllerをStaffLoginControllerとして分けている
- login.blade.phpをresources/views/staff/以下に分けて入れている
- dashboardを表示させるメソッドをStaffControllerに書いている
参考
https://qiita.com/nasteng/items/c6c026c3448a07a7fd15
https://qiita.com/nasteng/items/dfa9f6d252d887095f79
環境
Docker version 20.10.8,
docker-compose version 1.29.2,
PHP 7.4.25 (cli),
Composer version 2.1.9 2021-10-05 09:47:38,
Laravel Framework 8.68.0,
操作
モデル、コントローラ、マイグレーションの作成
$ ./vendor/bin/sail artisan make:Model Staff -mc
$ # php artisan make:Model Staff -mc (sailを使っていない場合)
Model created successfully.
Created Migration: 2021_11_20_223019_create_staff_table
Controller created successfully.
-mc
をつけると、モデルの作成と同時にマイグレーションとコントローラをつくってくれます。
staffは単複同形なのでtable名はstaffです。
2014_10_12_000000_create_users_table.php
からupメソッドをコピペ。
# (略)
public function up()
{
Schema::create('staff', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->foreignId('current_team_id')->nullable();
$table->string('profile_photo_path', 2048)->nullable();
$table->timestamps();
});
}
# (略)
マイグレーションの実行
$ ./vendor/bin/sail artisan migrate
Migrating: 2021_11_20_223019_create_staff_table
Migrated: 2021_11_20_223019_create_staff_table (180.33ms)
fillableプロパティの追加
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Staff extends Model
{
use HasFactory;
protected $fillable = [
'name', 'email', 'password',
];
}
シーダーの追加、実行
シーダーを作って、スタッフを登録してみます。
$ ./vendor/bin/sail artisan make:seeder StaffTableSeeder
Seeder created successfully.
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Staff;
use Illuminate\Support\Facades\Hash;
class StaffTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Staff::create([
'name' => 'staff1',
'email' => 'staff@example.com',
'password' => Hash::make('password')
]);
}
}
$ ./vendor/bin/sail artisan db:seed --class=StaffTableSeeder
Database seeding completed successfully.
TinkerでStaffが追加されているか確認
$ ./vendor/bin/sail tinker
Psy Shell v0.10.9 (PHP 7.4.25 — cli) by Justin Hileman
>>> App\Models\Staff::find(1)
=> App\Models\Staff {#4768
id: 1,
name: "staff1",
email: "staff@example.com",
email_verified_at: null,
password: "$2y$10$AXoh/wL5GKzAuPZ/FR9Hn.uI90FpiaCbrZcP6pL6bowVhHiazo4w6",
remember_token: null,
current_team_id: null,
profile_photo_path: null,
created_at: "2021-11-20 22:52:52",
updated_at: "2021-11-20 22:52:52",
}
>>> exit
Exit: Goodbye
スタッフ用のログイン機能を作る
AdminLoginResponseクラスの作成
app/Responses/AdminLoginResponse.php
を作成
内容は/multi-auth/vendor/laravel/fortify/src/Http/Responses/LoginResponse.php
からコピペして、リダイレクト先を編集。
ここでは、ログインが成功した時にstaff/dashboard
というルートにリダイレクトする。
<?php
namespace App\Responses;
use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract;
class StaffLoginResponse implements LoginResponseContract
{
/**
* Create an HTTP response that represents the object.
*
* @param \Illuminate\Http\Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function toResponse($request)
{
return $request->wantsJson()
? response()->json(['two_factor' => false])
: redirect()->intended('staff/dashboard'); // リダイレクト先を編集
}
}
スタッフ用のLoginControllerを作る
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Http\Request;
use Illuminate\Routing\Pipeline;
use App\Actions\Staff\AttemptToAuthenticate;
use Laravel\Fortify\Actions\PrepareAuthenticatedSession;
use App\Responses\StaffLoginResponse;
use Laravel\Fortify\Contracts\LogoutResponse;
use Laravel\Fortify\Http\Requests\LoginRequest;
class StaffLoginController extends Controller
{
/**
* The guard implementation.
*
* @var \Illuminate\Contracts\Auth\StatefulGuard
*/
protected $guard;
/**
* Create a new controller instance.
*
* @param \Illuminate\Contracts\Auth\StatefulGuard
* @return void
*/
public function __construct(StatefulGuard $guard)
{
$this->guard = $guard;
}
/**
* Show the login view.
*
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
*/
public function create()
{
return view('staff.login', ['guard' => 'staff']);
}
/**
* Attempt to authenticate a new session.
*
* @param \Laravel\Fortify\Http\Requests\LoginRequest $request
* @return mixed
*/
public function store(LoginRequest $request)
{
return $this->loginPipeline($request)->then(function ($request) {
return app(StaffLoginResponse::class);
});
}
/**
* Get the authentication pipeline instance.
*
* @param \Laravel\Fortify\Http\Requests\LoginRequest $request
* @return \Illuminate\Pipeline\Pipeline
*/
protected function loginPipeline(LoginRequest $request)
{
return (new Pipeline(app()))->send($request)->through(array_filter([
AttemptToAuthenticate::class,
PrepareAuthenticatedSession::class,
]));
}
/**
* Destroy an authenticated session.
*
* @param \Illuminate\Http\Request $request
* @return \Laravel\Fortify\Contracts\LogoutResponse
*/
public function destroy(Request $request): LogoutResponse
{
$this->guard->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return app(LogoutResponse::class);
}
}
AttemptToAuthenticateクラスを作成
app/Actions/Staff/以下に作成
<?php
namespace App\Actions\Staff;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Fortify;
use Laravel\Fortify\LoginRateLimiter;
class AttemptToAuthenticate
{
/**
* The guard implementation.
*
* @var \Illuminate\Contracts\Auth\StatefulGuard
*/
protected $guard;
/**
* The login rate limiter instance.
*
* @var \Laravel\Fortify\LoginRateLimiter
*/
protected $limiter;
/**
* Create a new controller instance.
*
* @param \Illuminate\Contracts\Auth\StatefulGuard $guard
* @param \Laravel\Fortify\LoginRateLimiter $limiter
* @return void
*/
public function __construct(StatefulGuard $guard, LoginRateLimiter $limiter)
{
$this->guard = $guard;
$this->limiter = $limiter;
}
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @param callable $next
* @return mixed
*/
public function handle($request, $next)
{
if (Fortify::$authenticateUsingCallback) {
return $this->handleUsingCustomCallback($request, $next);
}
if ($this->guard->attempt(
$request->only(Fortify::username(), 'password'),
$request->filled('remember')
)) {
return $next($request);
}
$this->throwFailedAuthenticationException($request);
}
/**
* Attempt to authenticate using a custom callback.
*
* @param \Illuminate\Http\Request $request
* @param callable $next
* @return mixed
*/
protected function handleUsingCustomCallback($request, $next)
{
$user = call_user_func(Fortify::$authenticateUsingCallback, $request);
if (!$user) {
return $this->throwFailedAuthenticationException($request);
}
$this->guard->login($user, $request->filled('remember'));
return $next($request);
}
/**
* Throw a failed authentication validation exception.
*
* @param \Illuminate\Http\Request $request
* @return void
*
* @throws \Illuminate\Validation\ValidationException
*/
protected function throwFailedAuthenticationException($request)
{
$this->limiter->increment($request);
throw ValidationException::withMessages([
Fortify::username() => [trans('auth.failed')],
]);
}
}
StaffLoginServiceProviderクラスの作成
<?php
namespace App\Providers;
use App\Http\Controllers\Auth\StaffLoginController; // 編集
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Support\Facades\Auth;
use App\Actions\Staff\AttemptToAuthenticate; // 編集
use Illuminate\Support\ServiceProvider;
class StaffLoginServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
$this->app
->when([StaffLoginController::class, AttemptToAuthenticate::class]) // 編集
->needs(StatefulGuard::class)
->give(function () {
return Auth::guard('staff');
});
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
//
}
}
app.php
の'providers'
にStaffLoginServiceProvider
を追加
# (略)
'providers' => [
# ...
App\Providers\StaffLoginServiceProvider::class,
],
# (略)
auth.phpの修正
<?php
return [
// 略
'guards' => [
// 略
'staff' => [
'driver' => 'session',
'provider' => 'staff',
],
// 略
],
'providers' => [
// 略
'staff' => [
'driver' => 'eloquent',
'model' => App\Models\Staff::class,
],
// 略
],
'passwords' => [
// 略
'staff' => [
'provider' => 'staff',
'table' => 'password_resets',
'expire' => 60,
'throttle' => 60,
],
],
// 略
];
web.phpの修正
ここでは、StaffControllerにdashboardメソッドを作って、ビューを返すようにする。
// 追加
use App\Http\Controllers\StaffController;
use App\Http\Controllers\Auth\StaffLoginController;
// 追加
Route::prefix('staff')->group(function () {
Route::get('/', function () {
if(Route::middleware('auth:staff')){
return redirect()->route('staff.dashboard');
}else{
return redirect()->route('staff.login');
}
});
Route::get('login', [StaffLoginController::class, 'create'])->name('staff.login');
Route::post('login', [StaffLoginController::class, 'store']);
Route::middleware('auth:staff')->group(function () {
Route::get('dashboard', [StaffController::class, 'dashboard'])->name('staff.dashboard');
});
});
ログイン画面のviewを作成
<x-guest-layout>
<x-jet-authentication-card>
<x-slot name="logo">
<x-jet-authentication-card-logo />
</x-slot>
<x-jet-validation-errors class="mb-4" />
@if (session('status'))
<div class="mb-4 font-medium text-sm text-green-600">
{{ session('status') }}
</div>
@endif
<form method="POST" action="{{ route('staff.login') }}">
@csrf
<div>
<x-jet-label for="email" value="{{ __('Email') }}" />
<x-jet-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')"
required autofocus />
</div>
<div class="mt-4">
<x-jet-label for="password" value="{{ __('Password') }}" />
<x-jet-input id="password" class="block mt-1 w-full" type="password" name="password" required
autocomplete="current-password" />
</div>
<div class="block mt-4">
<label for="remember_me" class="flex items-center">
<input id="remember_me" type="checkbox" class="form-checkbox" name="remember">
<span class="ml-2 text-sm text-gray-600">{{ __('Remember me') }}</span>
</label>
</div>
<div class="flex items-center justify-end mt-4">
@if (Route::has('password.request'))
<a class="underline text-sm text-gray-600 hover:text-gray-900" href="{{ route('password.request') }}">
{{ __('Forgot your password?') }}
</a>
@endif
<x-jet-button class="ml-4">
{{ __('Login') }}
</x-jet-button>
</div>
</form>
</x-jet-authentication-card>
</x-guest-layout>
ダッシュボードのメソッドとビューの作成
とりあえず、ユーザーのダッシュボードと同じ画面を表示できるようにしていきます。
// 追加
function dashboard()
{
return view('staff.dashboard');
}
デフォルトのダッシュボードのbladeをコピペして適宜書き換える。スロットでnavigation-menu
を埋め込む。ナビゲーションのbladeもコピペして適宜編集。
<x-app-layout>
<x-slot name="navigation">
{{ view('staff.navigation-menu') }}
</x-slot>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
<!-- わかりやすいようにここを修正-->
スタッフ用ダッシュボード
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
<x-jet-welcome />
</div>
</div>
</div>
</x-app-layout>
<nav x-data="{ open: false }" 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="flex-shrink-0 flex items-center">
<a href="{{ route('staff.dashboard') }}">
<img class="shadow-lg rounded-lg" src="/images/icon.png" alt="アイコン" width="50px" height="50px">
</a>
</div>
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
<x-nav-link href="{{ route('staff.dashboard') }}" :active="request()->routeIs('staff.dashboard')">
{{ __('スタッフ用ダッシュボード') }}
</x-nav-link>
</div>
</div>
<div class="hidden sm:flex sm:items-center sm:ml-6">
<!-- Teams Dropdown -->
@if (Laravel\Jetstream\Jetstream::hasTeamFeatures())
<div class="ml-3 relative">
<x-jet-dropdown align="right" width="60">
<x-slot name="trigger">
<span class="inline-flex rounded-md">
<button type="button" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:bg-gray-50 hover:text-gray-700 focus:outline-none focus:bg-gray-50 active:bg-gray-50 transition">
{{ Auth::user()->currentTeam->name }}
<svg class="ml-2 -mr-0.5 h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</span>
</x-slot>
<x-slot name="content">
<div class="w-60">
<!-- Team Management -->
<div class="block px-4 py-2 text-xs text-gray-400">
{{ __('Manage Team') }}
</div>
<!-- Team Settings -->
<x-jet-dropdown-link href="{{ route('teams.show', Auth::user()->currentTeam->id) }}">
{{ __('Team Settings') }}
</x-jet-dropdown-link>
@can('create', Laravel\Jetstream\Jetstream::newTeamModel())
<x-jet-dropdown-link href="{{ route('teams.create') }}">
{{ __('Create New Team') }}
</x-jet-dropdown-link>
@endcan
<div class="border-t border-gray-100"></div>
<!-- Team Switcher -->
<div class="block px-4 py-2 text-xs text-gray-400">
{{ __('Switch Teams') }}
</div>
@foreach (Auth::user()->allTeams() as $team)
<x-jet-switchable-team :team="$team" />
@endforeach
</div>
</x-slot>
</x-jet-dropdown>
</div>
@endif
<!-- Settings Dropdown -->
<div class="ml-3 relative">
<x-jet-dropdown align="right" width="48">
<x-slot name="trigger">
@if (Laravel\Jetstream\Jetstream::managesProfilePhotos())
<button class="flex text-sm border-2 border-transparent rounded-full focus:outline-none focus:border-gray-300 transition">
<img class="h-8 w-8 rounded-full object-cover" src="{{ Auth::user()->profile_photo_url }}" alt="{{ Auth::user()->name }}" />
</button>
@else
<span class="inline-flex rounded-md">
<button type="button" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition">
{{ Auth::user()->name }}
<svg class="ml-2 -mr-0.5 h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</span>
@endif
</x-slot>
<x-slot name="content">
<!-- Account Management -->
<div class="block px-4 py-2 text-xs text-gray-400">
{{ __('Manage Account') }}
</div>
{{-- <x-jet-dropdown-link href="{{ route('staff.profile.show') }}">
{{ __('Profile') }}
</x-jet-dropdown-link> --}}
{{-- @if (Laravel\Jetstream\Jetstream::hasApiFeatures())
<x-jet-dropdown-link href="{{ route('api-tokens.index') }}">
{{ __('API Tokens') }}
</x-jet-dropdown-link>
@endif --}}
<div class="border-t border-gray-100"></div>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-jet-dropdown-link href="{{ route('logout') }}"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-jet-dropdown-link>
</form>
</x-slot>
</x-jet-dropdown>
</div>
</div>
<!-- Hamburger -->
<div class="-mr-2 flex items-center sm:hidden">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
<!-- Responsive Navigation Menu -->
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
<div class="pt-2 pb-3 space-y-1">
<x-jet-responsive-nav-link href="{{ route('staff.dashboard') }}" :active="request()->routeIs('staff.dashboard')">
{{ __('Dashboard') }}
</x-jet-responsive-nav-link>
</div>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200">
<div class="flex items-center px-4">
@if (Laravel\Jetstream\Jetstream::managesProfilePhotos())
<div class="flex-shrink-0 mr-3">
<img class="h-10 w-10 rounded-full object-cover" src="{{ Auth::user()->profile_photo_url }}" alt="{{ Auth::user()->name }}" />
</div>
@endif
<div>
<div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
</div>
</div>
<div class="mt-3 space-y-1">
<!-- Account Management -->
{{-- <x-jet-responsive-nav-link href="{{ route('staff.profile.show') }}" :active="request()->routeIs('staff.profile.show')">
{{ __('Profile') }}
</x-jet-responsive-nav-link> --}}
@if (Laravel\Jetstream\Jetstream::hasApiFeatures())
{{-- <x-jet-responsive-nav-link href="{{ route('api-tokens.index') }}" :active="request()->routeIs('api-tokens.index')">
{{ __('API Tokens') }}
</x-jet-responsive-nav-link> --}}
@endif
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-jet-responsive-nav-link href="{{ route('logout') }}"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-jet-responsive-nav-link>
</form>
<!-- Team Management -->
@if (Laravel\Jetstream\Jetstream::hasTeamFeatures())
<div class="border-t border-gray-200"></div>
<div class="block px-4 py-2 text-xs text-gray-400">
{{ __('Manage Team') }}
</div>
<!-- Team Settings -->
<x-jet-responsive-nav-link href="{{ route('teams.show', Auth::user()->currentTeam->id) }}" :active="request()->routeIs('teams.show')">
{{ __('Team Settings') }}
</x-jet-responsive-nav-link>
@can('create', Laravel\Jetstream\Jetstream::newTeamModel())
<x-jet-responsive-nav-link href="{{ route('teams.create') }}" :active="request()->routeIs('teams.create')">
{{ __('Create New Team') }}
</x-jet-responsive-nav-link>
@endcan
<div class="border-t border-gray-200"></div>
<!-- Team Switcher -->
<div class="block px-4 py-2 text-xs text-gray-400">
{{ __('Switch Teams') }}
</div>
@foreach (Auth::user()->allTeams() as $team)
<x-jet-switchable-team :team="$team" component="jet-responsive-nav-link" />
@endforeach
@endif
</div>
</div>
</div>
</nav>