【Laravel】Laravel Breezeを使ってログイン認証機能を実装する|簡単な掲示板アプリの作成
Laravelの認証パッケージ「Breeze」を使い、掲示板アプリにログイン機能を実装する手順を解説します。ユーザー登録やログイン機能はもちろん、投稿した本人だけが記事を編集・削除できるようにするアクセス制御の方法まで、初心者にもわかりやすく説明します。
開発環境
- OS: Windows10
- Visual Studio Code: 1.73.0
- PHP: 8.3.11
- Laravel: 11.29.0
- laravel/breeze: 2.2
サンプルコード
/app/Http/Controllers/Auth/AuthenticatedSessionController.php
/app/Http/Controllers/Auth/AuthenticatedSessionController.php1+<?php 2+ 3+namespace App\Http\Controllers\Auth; 4+ 5+use App\Http\Controllers\Controller; 6+use App\Http\Requests\Auth\LoginRequest; 7+use Illuminate\Http\RedirectResponse; 8+use Illuminate\Http\Request; 9+use Illuminate\Support\Facades\Auth; 10+use Illuminate\View\View; 11+ 12+class AuthenticatedSessionController extends Controller 13+{ 14+ /** 15+ * Display the login view. 16+ */ 17+ public function create(): View 18+ { 19+ return view('auth.login'); 20+ } 21+ 22+ /** 23+ * Handle an incoming authentication request. 24+ */ 25+ public function store(LoginRequest $request): RedirectResponse 26+ { 27+ $request->authenticate(); 28+ 29+ $request->session()->regenerate(); 30+ 31+ // Login後のリダイレクト先をposts.indexに変更 32+ return redirect()->route('posts.index'); 33+ 34+ // return redirect()->intended(route('dashboard', absolute: false)); 35+ } 36+ 37+ /** 38+ * Destroy an authenticated session. 39+ */ 40+ public function destroy(Request $request): RedirectResponse 41+ { 42+ Auth::guard('web')->logout(); 43+ 44+ $request->session()->invalidate(); 45+ 46+ $request->session()->regenerateToken(); 47+ 48+ return redirect('/'); 49+ } 50+} 51
/app/Http/Controllers/Auth/ConfirmablePasswordController.php
/app/Http/Controllers/Auth/ConfirmablePasswordController.php1+<?php 2+ 3+namespace App\Http\Controllers\Auth; 4+ 5+use App\Http\Controllers\Controller; 6+use Illuminate\Http\RedirectResponse; 7+use Illuminate\Http\Request; 8+use Illuminate\Support\Facades\Auth; 9+use Illuminate\Validation\ValidationException; 10+use Illuminate\View\View; 11+ 12+class ConfirmablePasswordController extends Controller 13+{ 14+ /** 15+ * Show the confirm password view. 16+ */ 17+ public function show(): View 18+ { 19+ return view('auth.confirm-password'); 20+ } 21+ 22+ /** 23+ * Confirm the user's password. 24+ */ 25+ public function store(Request $request): RedirectResponse 26+ { 27+ if (! Auth::guard('web')->validate([ 28+ 'email' => $request->user()->email, 29+ 'password' => $request->password, 30+ ])) { 31+ throw ValidationException::withMessages([ 32+ 'password' => __('auth.password'), 33+ ]); 34+ } 35+ 36+ $request->session()->put('auth.password_confirmed_at', time()); 37+ 38+ return redirect()->intended(route('dashboard', absolute: false)); 39+ } 40+} 41
/app/Http/Controllers/Auth/EmailVerificationNotificationController.php
/app/Http/Controllers/Auth/EmailVerificationNotificationController.php1+<?php 2+ 3+namespace App\Http\Controllers\Auth; 4+ 5+use App\Http\Controllers\Controller; 6+use Illuminate\Http\RedirectResponse; 7+use Illuminate\Http\Request; 8+ 9+class EmailVerificationNotificationController extends Controller 10+{ 11+ /** 12+ * Send a new email verification notification. 13+ */ 14+ public function store(Request $request): RedirectResponse 15+ { 16+ if ($request->user()->hasVerifiedEmail()) { 17+ return redirect()->intended(route('dashboard', absolute: false)); 18+ } 19+ 20+ $request->user()->sendEmailVerificationNotification(); 21+ 22+ return back()->with('status', 'verification-link-sent'); 23+ } 24+} 25
/app/Http/Controllers/Auth/EmailVerificationPromptController.php
/app/Http/Controllers/Auth/EmailVerificationPromptController.php1+<?php 2+ 3+namespace App\Http\Controllers\Auth; 4+ 5+use App\Http\Controllers\Controller; 6+use Illuminate\Http\RedirectResponse; 7+use Illuminate\Http\Request; 8+use Illuminate\View\View; 9+ 10+class EmailVerificationPromptController extends Controller 11+{ 12+ /** 13+ * Display the email verification prompt. 14+ */ 15+ public function __invoke(Request $request): RedirectResponse|View 16+ { 17+ return $request->user()->hasVerifiedEmail() 18+ ? redirect()->intended(route('dashboard', absolute: false)) 19+ : view('auth.verify-email'); 20+ } 21+} 22
/app/Http/Controllers/Auth/NewPasswordController.php
/app/Http/Controllers/Auth/NewPasswordController.php1+<?php 2+ 3+namespace App\Http\Controllers\Auth; 4+ 5+use App\Http\Controllers\Controller; 6+use Illuminate\Auth\Events\PasswordReset; 7+use Illuminate\Http\RedirectResponse; 8+use Illuminate\Http\Request; 9+use Illuminate\Support\Facades\Hash; 10+use Illuminate\Support\Facades\Password; 11+use Illuminate\Support\Str; 12+use Illuminate\Validation\Rules; 13+use Illuminate\View\View; 14+ 15+class NewPasswordController extends Controller 16+{ 17+ /** 18+ * Display the password reset view. 19+ */ 20+ public function create(Request $request): View 21+ { 22+ return view('auth.reset-password', ['request' => $request]); 23+ } 24+ 25+ /** 26+ * Handle an incoming new password request. 27+ * 28+ * @throws \Illuminate\Validation\ValidationException 29+ */ 30+ public function store(Request $request): RedirectResponse 31+ { 32+ $request->validate([ 33+ 'token' => ['required'], 34+ 'email' => ['required', 'email'], 35+ 'password' => ['required', 'confirmed', Rules\Password::defaults()], 36+ ]); 37+ 38+ // Here we will attempt to reset the user's password. If it is successful we 39+ // will update the password on an actual user model and persist it to the 40+ // database. Otherwise we will parse the error and return the response. 41+ $status = Password::reset( 42+ $request->only('email', 'password', 'password_confirmation', 'token'), 43+ function ($user) use ($request) { 44+ $user->forceFill([ 45+ 'password' => Hash::make($request->password), 46+ 'remember_token' => Str::random(60), 47+ ])->save(); 48+ 49+ event(new PasswordReset($user)); 50+ } 51+ ); 52+ 53+ // If the password was successfully reset, we will redirect the user back to 54+ // the application's home authenticated view. If there is an error we can 55+ // redirect them back to where they came from with their error message. 56+ return $status == Password::PASSWORD_RESET 57+ ? redirect()->route('login')->with('status', __($status)) 58+ : back()->withInput($request->only('email')) 59+ ->withErrors(['email' => __($status)]); 60+ } 61+} 62
/app/Http/Controllers/Auth/PasswordController.php
/app/Http/Controllers/Auth/PasswordController.php1+<?php 2+ 3+namespace App\Http\Controllers\Auth; 4+ 5+use App\Http\Controllers\Controller; 6+use Illuminate\Http\RedirectResponse; 7+use Illuminate\Http\Request; 8+use Illuminate\Support\Facades\Hash; 9+use Illuminate\Validation\Rules\Password; 10+ 11+class PasswordController extends Controller 12+{ 13+ /** 14+ * Update the user's password. 15+ */ 16+ public function update(Request $request): RedirectResponse 17+ { 18+ $validated = $request->validateWithBag('updatePassword', [ 19+ 'current_password' => ['required', 'current_password'], 20+ 'password' => ['required', Password::defaults(), 'confirmed'], 21+ ]); 22+ 23+ $request->user()->update([ 24+ 'password' => Hash::make($validated['password']), 25+ ]); 26+ 27+ return back()->with('status', 'password-updated'); 28+ } 29+} 30
/app/Http/Controllers/Auth/PasswordResetLinkController.php
/app/Http/Controllers/Auth/PasswordResetLinkController.php1+<?php 2+ 3+namespace App\Http\Controllers\Auth; 4+ 5+use App\Http\Controllers\Controller; 6+use Illuminate\Http\RedirectResponse; 7+use Illuminate\Http\Request; 8+use Illuminate\Support\Facades\Password; 9+use Illuminate\View\View; 10+ 11+class PasswordResetLinkController extends Controller 12+{ 13+ /** 14+ * Display the password reset link request view. 15+ */ 16+ public function create(): View 17+ { 18+ return view('auth.forgot-password'); 19+ } 20+ 21+ /** 22+ * Handle an incoming password reset link request. 23+ * 24+ * @throws \Illuminate\Validation\ValidationException 25+ */ 26+ public function store(Request $request): RedirectResponse 27+ { 28+ $request->validate([ 29+ 'email' => ['required', 'email'], 30+ ]); 31+ 32+ // We will send the password reset link to this user. Once we have attempted 33+ // to send the link, we will examine the response then see the message we 34+ // need to show to the user. Finally, we'll send out a proper response. 35+ $status = Password::sendResetLink( 36+ $request->only('email') 37+ ); 38+ 39+ return $status == Password::RESET_LINK_SENT 40+ ? back()->with('status', __($status)) 41+ : back()->withInput($request->only('email')) 42+ ->withErrors(['email' => __($status)]); 43+ } 44+} 45
/app/Http/Controllers/Auth/RegisteredUserController.php
/app/Http/Controllers/Auth/RegisteredUserController.php1+<?php 2+ 3+namespace App\Http\Controllers\Auth; 4+ 5+use App\Http\Controllers\Controller; 6+use App\Models\User; 7+use Illuminate\Auth\Events\Registered; 8+use Illuminate\Http\RedirectResponse; 9+use Illuminate\Http\Request; 10+use Illuminate\Support\Facades\Auth; 11+use Illuminate\Support\Facades\Hash; 12+use Illuminate\Validation\Rules; 13+use Illuminate\View\View; 14+ 15+class RegisteredUserController extends Controller 16+{ 17+ /** 18+ * Display the registration view. 19+ */ 20+ public function create(): View 21+ { 22+ return view('auth.register'); 23+ } 24+ 25+ /** 26+ * Handle an incoming registration request. 27+ * 28+ * @throws \Illuminate\Validation\ValidationException 29+ */ 30+ public function store(Request $request): RedirectResponse 31+ { 32+ $request->validate([ 33+ 'name' => ['required', 'string', 'max:255'], 34+ 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], 35+ 'password' => ['required', 'confirmed', Rules\Password::defaults()], 36+ ]); 37+ 38+ $user = User::create([ 39+ 'name' => $request->name, 40+ 'email' => $request->email, 41+ 'password' => Hash::make($request->password), 42+ ]); 43+ 44+ event(new Registered($user)); 45+ 46+ Auth::login($user); 47+ 48+ // 新規登録後にリダイレクトするURLを指定 49+ return redirect()->route('posts.index'); 50+ 51+ // return redirect(route('dashboard', absolute: false)); 52+ } 53+} 54
/app/Http/Controllers/Auth/VerifyEmailController.php
/app/Http/Controllers/Auth/VerifyEmailController.php1+<?php 2+ 3+namespace App\Http\Controllers\Auth; 4+ 5+use App\Http\Controllers\Controller; 6+use Illuminate\Auth\Events\Verified; 7+use Illuminate\Foundation\Auth\EmailVerificationRequest; 8+use Illuminate\Http\RedirectResponse; 9+ 10+class VerifyEmailController extends Controller 11+{ 12+ /** 13+ * Mark the authenticated user's email address as verified. 14+ */ 15+ public function __invoke(EmailVerificationRequest $request): RedirectResponse 16+ { 17+ if ($request->user()->hasVerifiedEmail()) { 18+ return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); 19+ } 20+ 21+ if ($request->user()->markEmailAsVerified()) { 22+ event(new Verified($request->user())); 23+ } 24+ 25+ return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); 26+ } 27+} 28
/app/Http/Controllers/PostController.php
/app/Http/Controllers/PostController.php1 2 use Illuminate\Http\Request; 3 use App\Models\Post; 4+use Illuminate\Support\Facades\Auth; 5 6 class PostController extends Controller 7 { 8 */ 9 public function create() 10 { 11+ // ログインユーザーのみが新規作成ページにアクセス可能 12+ if (!Auth::check()) { 13+ return redirect()->route('login'); 14+ } 15+ 16 return view('posts.create'); 17 } 18 19 */ 20 public function store(Request $request) 21 { 22+ // ログインユーザーのみが投稿可能 23+ if (!Auth::check()) { 24+ return redirect()->route('login'); 25+ } 26+ 27 $request->validate([ 28 'title' => 'required|max:255', 29 'content' => 'required' 30 ]); 31 32- $post = Post::create($request->all()); 33+ $post = Post::create([ 34+ 'title' => $request->title, 35+ 'content' => $request->content, 36+ 'user_id' => auth()->id() 37+ ]); 38 39 return redirect()->route('posts.show', ['post' => $post->id]); 40 } 41 public function edit(string $id) 42 { 43 $post = Post::findOrFail($id); 44+ 45+ // 所有者のみ編集ページにアクセス可能 46+ if (Auth::id() !== $post->user_id) { 47+ return redirect()->route('posts.index'); 48+ } 49+ 50 return view('posts.edit', compact('post')); 51 } 52 53 54 $post = Post::findOrFail($id); 55 56+ // 所有者のみ更新可能 57+ if (Auth::id() !== $post->user_id) { 58+ return redirect()->route('posts.index'); 59+ } 60+ 61 $post->update([ 62 'title' => $request->title, 63 'content' => $request->content 64 public function destroy(string $id) 65 { 66 $post = Post::findOrFail($id); 67+ 68+ // 所有者のみ削除可能 69+ if (Auth::id() !== $post->user_id) { 70+ return redirect()->route('posts.index'); 71+ } 72+ 73 $post->delete(); 74+ 75 return redirect()->route('posts.index'); 76 } 77 } 78
/app/Http/Controllers/ProfileController.php
/app/Http/Controllers/ProfileController.php1+<?php 2+ 3+namespace App\Http\Controllers; 4+ 5+use App\Http\Requests\ProfileUpdateRequest; 6+use Illuminate\Http\RedirectResponse; 7+use Illuminate\Http\Request; 8+use Illuminate\Support\Facades\Auth; 9+use Illuminate\Support\Facades\Redirect; 10+use Illuminate\View\View; 11+ 12+class ProfileController extends Controller 13+{ 14+ /** 15+ * Display the user's profile form. 16+ */ 17+ public function edit(Request $request): View 18+ { 19+ return view('profile.edit', [ 20+ 'user' => $request->user(), 21+ ]); 22+ } 23+ 24+ /** 25+ * Update the user's profile information. 26+ */ 27+ public function update(ProfileUpdateRequest $request): RedirectResponse 28+ { 29+ $request->user()->fill($request->validated()); 30+ 31+ if ($request->user()->isDirty('email')) { 32+ $request->user()->email_verified_at = null; 33+ } 34+ 35+ $request->user()->save(); 36+ 37+ return Redirect::route('profile.edit')->with('status', 'profile-updated'); 38+ } 39+ 40+ /** 41+ * Delete the user's account. 42+ */ 43+ public function destroy(Request $request): RedirectResponse 44+ { 45+ $request->validateWithBag('userDeletion', [ 46+ 'password' => ['required', 'current_password'], 47+ ]); 48+ 49+ $user = $request->user(); 50+ 51+ Auth::logout(); 52+ 53+ $user->delete(); 54+ 55+ $request->session()->invalidate(); 56+ $request->session()->regenerateToken(); 57+ 58+ return Redirect::to('/'); 59+ } 60+} 61
/app/Http/Requests/Auth/LoginRequest.php
/app/Http/Requests/Auth/LoginRequest.php1+<?php 2+ 3+namespace App\Http\Requests\Auth; 4+ 5+use Illuminate\Auth\Events\Lockout; 6+use Illuminate\Foundation\Http\FormRequest; 7+use Illuminate\Support\Facades\Auth; 8+use Illuminate\Support\Facades\RateLimiter; 9+use Illuminate\Support\Str; 10+use Illuminate\Validation\ValidationException; 11+ 12+class LoginRequest extends FormRequest 13+{ 14+ /** 15+ * Determine if the user is authorized to make this request. 16+ */ 17+ public function authorize(): bool 18+ { 19+ return true; 20+ } 21+ 22+ /** 23+ * Get the validation rules that apply to the request. 24+ * 25+ * @return array<string, \Illuminate\Contracts\Validation\Rule|array|string> 26+ */ 27+ public function rules(): array 28+ { 29+ return [ 30+ 'email' => ['required', 'string', 'email'], 31+ 'password' => ['required', 'string'], 32+ ]; 33+ } 34+ 35+ /** 36+ * Attempt to authenticate the request's credentials. 37+ * 38+ * @throws \Illuminate\Validation\ValidationException 39+ */ 40+ public function authenticate(): void 41+ { 42+ $this->ensureIsNotRateLimited(); 43+ 44+ if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { 45+ RateLimiter::hit($this->throttleKey()); 46+ 47+ throw ValidationException::withMessages([ 48+ 'email' => trans('auth.failed'), 49+ ]); 50+ } 51+ 52+ RateLimiter::clear($this->throttleKey()); 53+ } 54+ 55+ /** 56+ * Ensure the login request is not rate limited. 57+ * 58+ * @throws \Illuminate\Validation\ValidationException 59+ */ 60+ public function ensureIsNotRateLimited(): void 61+ { 62+ if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { 63+ return; 64+ } 65+ 66+ event(new Lockout($this)); 67+ 68+ $seconds = RateLimiter::availableIn($this->throttleKey()); 69+ 70+ throw ValidationException::withMessages([ 71+ 'email' => trans('auth.throttle', [ 72+ 'seconds' => $seconds, 73+ 'minutes' => ceil($seconds / 60), 74+ ]), 75+ ]); 76+ } 77+ 78+ /** 79+ * Get the rate limiting throttle key for the request. 80+ */ 81+ public function throttleKey(): string 82+ { 83+ return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip()); 84+ } 85+} 86
/app/Http/Requests/ProfileUpdateRequest.php
/app/Http/Requests/ProfileUpdateRequest.php1+<?php 2+ 3+namespace App\Http\Requests; 4+ 5+use App\Models\User; 6+use Illuminate\Foundation\Http\FormRequest; 7+use Illuminate\Validation\Rule; 8+ 9+class ProfileUpdateRequest extends FormRequest 10+{ 11+ /** 12+ * Get the validation rules that apply to the request. 13+ * 14+ * @return array<string, \Illuminate\Contracts\Validation\Rule|array|string> 15+ */ 16+ public function rules(): array 17+ { 18+ return [ 19+ 'name' => ['required', 'string', 'max:255'], 20+ 'email' => [ 21+ 'required', 22+ 'string', 23+ 'lowercase', 24+ 'email', 25+ 'max:255', 26+ Rule::unique(User::class)->ignore($this->user()->id), 27+ ], 28+ ]; 29+ } 30+} 31
/app/Models/Post.php
/app/Models/Post.php1 { 2 protected $fillable = [ 3 'title', 4- 'content' 5+ 'content', 6+ 'user_id' 7 ]; 8 } 9
/app/View/Components/AppLayout.php
/app/View/Components/AppLayout.php1+<?php 2+ 3+namespace App\View\Components; 4+ 5+use Illuminate\View\Component; 6+use Illuminate\View\View; 7+ 8+class AppLayout extends Component 9+{ 10+ /** 11+ * Get the view / contents that represents the component. 12+ */ 13+ public function render(): View 14+ { 15+ return view('layouts.app'); 16+ } 17+} 18
/app/View/Components/GuestLayout.php
/app/View/Components/GuestLayout.php1+<?php 2+ 3+namespace App\View\Components; 4+ 5+use Illuminate\View\Component; 6+use Illuminate\View\View; 7+ 8+class GuestLayout extends Component 9+{ 10+ /** 11+ * Get the view / contents that represents the component. 12+ */ 13+ public function render(): View 14+ { 15+ return view('layouts.guest'); 16+ } 17+} 18
/composer.json
/composer.json1 }, 2 "require-dev": { 3 "fakerphp/faker": "^1.23", 4+ "laravel/breeze": "^2.2", 5 "laravel/pail": "^1.1", 6 "laravel/pint": "^1.13", 7 "laravel/sail": "^1.26", 8
/database/migrations/2024_10_26_120312_add_user_id_to_posts_table.php
/database/migrations/2024_10_26_120312_add_user_id_to_posts_table.php1+<?php 2+ 3+use Illuminate\Database\Migrations\Migration; 4+use Illuminate\Database\Schema\Blueprint; 5+use Illuminate\Support\Facades\Schema; 6+ 7+return new class extends Migration 8+{ 9+ /** 10+ * Run the migrations. 11+ */ 12+ public function up(): void 13+ { 14+ Schema::table('posts', function (Blueprint $table) { 15+ $table->foreignId('user_id')->nullable()->constrained()->onDelete('cascade'); 16+ }); 17+ } 18+ 19+ /** 20+ * Reverse the migrations. 21+ */ 22+ public function down(): void 23+ { 24+ Schema::table('posts', function (Blueprint $table) { 25+ $table->dropForeign(['user_id']); 26+ $table->dropColumn('user_id'); 27+ }); 28+ } 29+}; 30
/package.json
/package.json1 "dev": "vite" 2 }, 3 "devDependencies": { 4- "autoprefixer": "^10.4.20", 5+ "@tailwindcss/forms": "^0.5.2", 6+ "alpinejs": "^3.4.2", 7+ "autoprefixer": "^10.4.2", 8 "axios": "^1.7.4", 9 "concurrently": "^9.0.1", 10 "laravel-vite-plugin": "^1.0", 11- "postcss": "^8.4.47", 12- "tailwindcss": "^3.4.13", 13+ "postcss": "^8.4.31", 14+ "tailwindcss": "^3.1.0", 15 "vite": "^5.0" 16 } 17 } 18
/resources/js/app.js
/resources/js/app.js1 import './bootstrap'; 2+ 3+import Alpine from 'alpinejs'; 4+ 5+window.Alpine = Alpine; 6+ 7+Alpine.start(); 8
/resources/views/auth/confirm-password.blade.php
/resources/views/auth/confirm-password.blade.php1+<x-guest-layout> 2+ <div class="mb-4 text-sm text-gray-600 dark:text-gray-400"> 3+ {{ __('This is a secure area of the application. Please confirm your password before continuing.') }} 4+ </div> 5+ 6+ <form method="POST" action="{{ route('password.confirm') }}"> 7+ @csrf 8+ 9+ <!-- Password --> 10+ <div> 11+ <x-input-label for="password" :value="__('Password')" /> 12+ 13+ <x-text-input id="password" class="block mt-1 w-full" 14+ type="password" 15+ name="password" 16+ required autocomplete="current-password" /> 17+ 18+ <x-input-error :messages="$errors->get('password')" class="mt-2" /> 19+ </div> 20+ 21+ <div class="flex justify-end mt-4"> 22+ <x-primary-button> 23+ {{ __('Confirm') }} 24+ </x-primary-button> 25+ </div> 26+ </form> 27+</x-guest-layout> 28
/resources/views/auth/forgot-password.blade.php
/resources/views/auth/forgot-password.blade.php1+<x-guest-layout> 2+ <div class="mb-4 text-sm text-gray-600 dark:text-gray-400"> 3+ {{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }} 4+ </div> 5+ 6+ <!-- Session Status --> 7+ <x-auth-session-status class="mb-4" :status="session('status')" /> 8+ 9+ <form method="POST" action="{{ route('password.email') }}"> 10+ @csrf 11+ 12+ <!-- Email Address --> 13+ <div> 14+ <x-input-label for="email" :value="__('Email')" /> 15+ <x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus /> 16+ <x-input-error :messages="$errors->get('email')" class="mt-2" /> 17+ </div> 18+ 19+ <div class="flex items-center justify-end mt-4"> 20+ <x-primary-button> 21+ {{ __('Email Password Reset Link') }} 22+ </x-primary-button> 23+ </div> 24+ </form> 25+</x-guest-layout> 26
/resources/views/auth/login.blade.php
/resources/views/auth/login.blade.php1+<x-guest-layout> 2+ <!-- Session Status --> 3+ <x-auth-session-status class="mb-4" :status="session('status')" /> 4+ 5+ <form method="POST" action="{{ route('login') }}"> 6+ @csrf 7+ 8+ <!-- Email Address --> 9+ <div> 10+ <x-input-label for="email" :value="__('Email')" /> 11+ <x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" /> 12+ <x-input-error :messages="$errors->get('email')" class="mt-2" /> 13+ </div> 14+ 15+ <!-- Password --> 16+ <div class="mt-4"> 17+ <x-input-label for="password" :value="__('Password')" /> 18+ 19+ <x-text-input id="password" class="block mt-1 w-full" 20+ type="password" 21+ name="password" 22+ required autocomplete="current-password" /> 23+ 24+ <x-input-error :messages="$errors->get('password')" class="mt-2" /> 25+ </div> 26+ 27+ <!-- Remember Me --> 28+ <div class="block mt-4"> 29+ <label for="remember_me" class="inline-flex items-center"> 30+ <input id="remember_me" type="checkbox" class="rounded dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800" name="remember"> 31+ <span class="ms-2 text-sm text-gray-600 dark:text-gray-400">{{ __('Remember me') }}</span> 32+ </label> 33+ </div> 34+ 35+ <div class="flex items-center justify-end mt-4"> 36+ @if (Route::has('password.request')) 37+ <a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800" href="{{ route('password.request') }}"> 38+ {{ __('Forgot your password?') }} 39+ </a> 40+ @endif 41+ 42+ <x-primary-button class="ms-3"> 43+ {{ __('Log in') }} 44+ </x-primary-button> 45+ </div> 46+ </form> 47+</x-guest-layout> 48
/resources/views/auth/register.blade.php
/resources/views/auth/register.blade.php1+<x-guest-layout> 2+ <form method="POST" action="{{ route('register') }}"> 3+ @csrf 4+ 5+ <!-- Name --> 6+ <div> 7+ <x-input-label for="name" :value="__('Name')" /> 8+ <x-text-input id="name" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required autofocus autocomplete="name" /> 9+ <x-input-error :messages="$errors->get('name')" class="mt-2" /> 10+ </div> 11+ 12+ <!-- Email Address --> 13+ <div class="mt-4"> 14+ <x-input-label for="email" :value="__('Email')" /> 15+ <x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autocomplete="username" /> 16+ <x-input-error :messages="$errors->get('email')" class="mt-2" /> 17+ </div> 18+ 19+ <!-- Password --> 20+ <div class="mt-4"> 21+ <x-input-label for="password" :value="__('Password')" /> 22+ 23+ <x-text-input id="password" class="block mt-1 w-full" 24+ type="password" 25+ name="password" 26+ required autocomplete="new-password" /> 27+ 28+ <x-input-error :messages="$errors->get('password')" class="mt-2" /> 29+ </div> 30+ 31+ <!-- Confirm Password --> 32+ <div class="mt-4"> 33+ <x-input-label for="password_confirmation" :value="__('Confirm Password')" /> 34+ 35+ <x-text-input id="password_confirmation" class="block mt-1 w-full" 36+ type="password" 37+ name="password_confirmation" required autocomplete="new-password" /> 38+ 39+ <x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" /> 40+ </div> 41+ 42+ <div class="flex items-center justify-end mt-4"> 43+ <a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800" href="{{ route('login') }}"> 44+ {{ __('Already registered?') }} 45+ </a> 46+ 47+ <x-primary-button class="ms-4"> 48+ {{ __('Register') }} 49+ </x-primary-button> 50+ </div> 51+ </form> 52+</x-guest-layout> 53
/resources/views/auth/reset-password.blade.php
/resources/views/auth/reset-password.blade.php1+<x-guest-layout> 2+ <form method="POST" action="{{ route('password.store') }}"> 3+ @csrf 4+ 5+ <!-- Password Reset Token --> 6+ <input type="hidden" name="token" value="{{ $request->route('token') }}"> 7+ 8+ <!-- Email Address --> 9+ <div> 10+ <x-input-label for="email" :value="__('Email')" /> 11+ <x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required autofocus autocomplete="username" /> 12+ <x-input-error :messages="$errors->get('email')" class="mt-2" /> 13+ </div> 14+ 15+ <!-- Password --> 16+ <div class="mt-4"> 17+ <x-input-label for="password" :value="__('Password')" /> 18+ <x-text-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" /> 19+ <x-input-error :messages="$errors->get('password')" class="mt-2" /> 20+ </div> 21+ 22+ <!-- Confirm Password --> 23+ <div class="mt-4"> 24+ <x-input-label for="password_confirmation" :value="__('Confirm Password')" /> 25+ 26+ <x-text-input id="password_confirmation" class="block mt-1 w-full" 27+ type="password" 28+ name="password_confirmation" required autocomplete="new-password" /> 29+ 30+ <x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" /> 31+ </div> 32+ 33+ <div class="flex items-center justify-end mt-4"> 34+ <x-primary-button> 35+ {{ __('Reset Password') }} 36+ </x-primary-button> 37+ </div> 38+ </form> 39+</x-guest-layout> 40
/resources/views/auth/verify-email.blade.php
/resources/views/auth/verify-email.blade.php1+<x-guest-layout> 2+ <div class="mb-4 text-sm text-gray-600 dark:text-gray-400"> 3+ {{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }} 4+ </div> 5+ 6+ @if (session('status') == 'verification-link-sent') 7+ <div class="mb-4 font-medium text-sm text-green-600 dark:text-green-400"> 8+ {{ __('A new verification link has been sent to the email address you provided during registration.') }} 9+ </div> 10+ @endif 11+ 12+ <div class="mt-4 flex items-center justify-between"> 13+ <form method="POST" action="{{ route('verification.send') }}"> 14+ @csrf 15+ 16+ <div> 17+ <x-primary-button> 18+ {{ __('Resend Verification Email') }} 19+ </x-primary-button> 20+ </div> 21+ </form> 22+ 23+ <form method="POST" action="{{ route('logout') }}"> 24+ @csrf 25+ 26+ <button type="submit" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800"> 27+ {{ __('Log Out') }} 28+ </button> 29+ </form> 30+ </div> 31+</x-guest-layout> 32
/resources/views/components/application-logo.blade.php
/resources/views/components/application-logo.blade.php1+<svg viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}> 2+ <path d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z"/> 3+</svg> 4
/resources/views/components/auth-session-status.blade.php
/resources/views/components/auth-session-status.blade.php1+@props(['status']) 2+ 3+@if ($status) 4+ <div {{ $attributes->merge(['class' => 'font-medium text-sm text-green-600 dark:text-green-400']) }}> 5+ {{ $status }} 6+ </div> 7+@endif 8
/resources/views/components/danger-button.blade.php
/resources/views/components/danger-button.blade.php1+<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150']) }}> 2+ {{ $slot }} 3+</button> 4
/resources/views/components/dropdown-link.blade.php
/resources/views/components/dropdown-link.blade.php1+<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-800 transition duration-150 ease-in-out']) }}>{{ $slot }}</a> 2
/resources/views/components/dropdown.blade.php
/resources/views/components/dropdown.blade.php1+@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white dark:bg-gray-700']) 2+ 3+@php 4+$alignmentClasses = match ($align) { 5+ 'left' => 'ltr:origin-top-left rtl:origin-top-right start-0', 6+ 'top' => 'origin-top', 7+ default => 'ltr:origin-top-right rtl:origin-top-left end-0', 8+}; 9+ 10+$width = match ($width) { 11+ '48' => 'w-48', 12+ default => $width, 13+}; 14+@endphp 15+ 16+<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false"> 17+ <div @click="open = ! open"> 18+ {{ $trigger }} 19+ </div> 20+ 21+ <div x-show="open" 22+ x-transition:enter="transition ease-out duration-200" 23+ x-transition:enter-start="opacity-0 scale-95" 24+ x-transition:enter-end="opacity-100 scale-100" 25+ x-transition:leave="transition ease-in duration-75" 26+ x-transition:leave-start="opacity-100 scale-100" 27+ x-transition:leave-end="opacity-0 scale-95" 28+ class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}" 29+ style="display: none;" 30+ @click="open = false"> 31+ <div class="rounded-md ring-1 ring-black ring-opacity-5 {{ $contentClasses }}"> 32+ {{ $content }} 33+ </div> 34+ </div> 35+</div> 36
/resources/views/components/input-error.blade.php
/resources/views/components/input-error.blade.php1+@props(['messages']) 2+ 3+@if ($messages) 4+ <ul {{ $attributes->merge(['class' => 'text-sm text-red-600 dark:text-red-400 space-y-1']) }}> 5+ @foreach ((array) $messages as $message) 6+ <li>{{ $message }}</li> 7+ @endforeach 8+ </ul> 9+@endif 10
/resources/views/components/input-label.blade.php
/resources/views/components/input-label.blade.php1+@props(['value']) 2+ 3+<label {{ $attributes->merge(['class' => 'block font-medium text-sm text-gray-700 dark:text-gray-300']) }}> 4+ {{ $value ?? $slot }} 5+</label> 6
/resources/views/components/modal.blade.php
/resources/views/components/modal.blade.php1+@props([ 2+ 'name', 3+ 'show' => false, 4+ 'maxWidth' => '2xl' 5+]) 6+ 7+@php 8+$maxWidth = [ 9+ 'sm' => 'sm:max-w-sm', 10+ 'md' => 'sm:max-w-md', 11+ 'lg' => 'sm:max-w-lg', 12+ 'xl' => 'sm:max-w-xl', 13+ '2xl' => 'sm:max-w-2xl', 14+][$maxWidth]; 15+@endphp 16+ 17+<div 18+ x-data="{ 19+ show: @js($show), 20+ focusables() { 21+ // All focusable element types... 22+ let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])' 23+ return [...$el.querySelectorAll(selector)] 24+ // All non-disabled elements... 25+ .filter(el => ! el.hasAttribute('disabled')) 26+ }, 27+ firstFocusable() { return this.focusables()[0] }, 28+ lastFocusable() { return this.focusables().slice(-1)[0] }, 29+ nextFocusable() { return this.focusables()[this.nextFocusableIndex()] || this.firstFocusable() }, 30+ prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() }, 31+ nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) }, 32+ prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 }, 33+ }" 34+ x-init="$watch('show', value => { 35+ if (value) { 36+ document.body.classList.add('overflow-y-hidden'); 37+ {{ $attributes->has('focusable') ? 'setTimeout(() => firstFocusable().focus(), 100)' : '' }} 38+ } else { 39+ document.body.classList.remove('overflow-y-hidden'); 40+ } 41+ })" 42+ x-on:open-modal.window="$event.detail == '{{ $name }}' ? show = true : null" 43+ x-on:close-modal.window="$event.detail == '{{ $name }}' ? show = false : null" 44+ x-on:close.stop="show = false" 45+ x-on:keydown.escape.window="show = false" 46+ x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()" 47+ x-on:keydown.shift.tab.prevent="prevFocusable().focus()" 48+ x-show="show" 49+ class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50" 50+ style="display: {{ $show ? 'block' : 'none' }};" 51+> 52+ <div 53+ x-show="show" 54+ class="fixed inset-0 transform transition-all" 55+ x-on:click="show = false" 56+ x-transition:enter="ease-out duration-300" 57+ x-transition:enter-start="opacity-0" 58+ x-transition:enter-end="opacity-100" 59+ x-transition:leave="ease-in duration-200" 60+ x-transition:leave-start="opacity-100" 61+ x-transition:leave-end="opacity-0" 62+ > 63+ <div class="absolute inset-0 bg-gray-500 dark:bg-gray-900 opacity-75"></div> 64+ </div> 65+ 66+ <div 67+ x-show="show" 68+ class="mb-6 bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto" 69+ x-transition:enter="ease-out duration-300" 70+ x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" 71+ x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" 72+ x-transition:leave="ease-in duration-200" 73+ x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100" 74+ x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" 75+ > 76+ {{ $slot }} 77+ </div> 78+</div> 79
/resources/views/components/nav-link.blade.php
/resources/views/components/nav-link.blade.php1+@props(['active']) 2+ 3+@php 4+$classes = ($active ?? false) 5+ ? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 dark:border-indigo-600 text-sm font-medium leading-5 text-gray-900 dark:text-gray-100 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out' 6+ : 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-700 focus:outline-none focus:text-gray-700 dark:focus:text-gray-300 focus:border-gray-300 dark:focus:border-gray-700 transition duration-150 ease-in-out'; 7+@endphp 8+ 9+<a {{ $attributes->merge(['class' => $classes]) }}> 10+ {{ $slot }} 11+</a> 12
/resources/views/components/primary-button.blade.php
/resources/views/components/primary-button.blade.php1+<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-white focus:bg-gray-700 dark:focus:bg-white active:bg-gray-900 dark:active:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150']) }}> 2+ {{ $slot }} 3+</button> 4
/resources/views/components/responsive-nav-link.blade.php
/resources/views/components/responsive-nav-link.blade.php1+@props(['active']) 2+ 3+@php 4+$classes = ($active ?? false) 5+ ? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 dark:border-indigo-600 text-start text-base font-medium text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/50 focus:outline-none focus:text-indigo-800 dark:focus:text-indigo-200 focus:bg-indigo-100 dark:focus:bg-indigo-900 focus:border-indigo-700 dark:focus:border-indigo-300 transition duration-150 ease-in-out' 6+ : 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-600 focus:outline-none focus:text-gray-800 dark:focus:text-gray-200 focus:bg-gray-50 dark:focus:bg-gray-700 focus:border-gray-300 dark:focus:border-gray-600 transition duration-150 ease-in-out'; 7+@endphp 8+ 9+<a {{ $attributes->merge(['class' => $classes]) }}> 10+ {{ $slot }} 11+</a> 12
/resources/views/components/secondary-button.blade.php
/resources/views/components/secondary-button.blade.php1+<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150']) }}> 2+ {{ $slot }} 3+</button> 4
/resources/views/components/text-input.blade.php
/resources/views/components/text-input.blade.php1+@props(['disabled' => false]) 2+ 3+<input @disabled($disabled) {{ $attributes->merge(['class' => 'border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm']) }}> 4
/resources/views/dashboard.blade.php
/resources/views/dashboard.blade.php1+<x-app-layout> 2+ <x-slot name="header"> 3+ <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight"> 4+ {{ __('Dashboard') }} 5+ </h2> 6+ </x-slot> 7+ 8+ <div class="py-12"> 9+ <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> 10+ <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg"> 11+ <div class="p-6 text-gray-900 dark:text-gray-100"> 12+ {{ __("You're logged in!") }} 13+ </div> 14+ </div> 15+ </div> 16+ </div> 17+</x-app-layout> 18
/resources/views/layout.blade.php
/resources/views/layout.blade.php1 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 2 <title>掲示板アプリ</title> 3 <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> 4+ {{-- <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script> 5+ <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.0.0/dist/umd/popper.min.js"></script> 6+ <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> --}} 7 </head> 8 <body> 9- @yield('content') 10+ <nav class="navbar navbar-expand-lg bg-primary"> 11+ <div class="container"> 12+ <a class="navbar-brand text-white" href="{{ route('posts.index') }}">掲示板アプリ</a> 13+ <div class="ml-auto"> 14+ <ul class="navbar-nav"> 15+ @if (Auth::check()) 16+ <li class="nav-item"> 17+ <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;"> 18+ @csrf 19+ </form> 20+ <a class="nav-link text-white" href="#" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">ログアウト</a> 21+ </li> 22+ @else 23+ <li class="nav-item"> 24+ <a class="nav-link text-white" href="{{ route('login') }}">ログイン</a> 25+ </li> 26+ <li class="nav-item"> 27+ <a class="nav-link text-white" href="{{ route('register') }}">新規登録</a> 28+ </li> 29+ @endif 30+ </ul> 31+ </div> 32+ </div> 33+ </nav> 34+ <div class="container mt-4"> 35+ @yield('content') 36+ </div> 37 </body> 38 </html> 39
/resources/views/layouts/app.blade.php
/resources/views/layouts/app.blade.php1+<!DOCTYPE html> 2+<html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> 3+ <head> 4+ <meta charset="utf-8"> 5+ <meta name="viewport" content="width=device-width, initial-scale=1"> 6+ <meta name="csrf-token" content="{{ csrf_token() }}"> 7+ 8+ <title>{{ config('app.name', 'Laravel') }}</title> 9+ 10+ <!-- Fonts --> 11+ <link rel="preconnect" href="https://fonts.bunny.net"> 12+ <link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" /> 13+ 14+ <!-- Scripts --> 15+ @vite(['resources/css/app.css', 'resources/js/app.js']) 16+ </head> 17+ <body class="font-sans antialiased"> 18+ <div class="min-h-screen bg-gray-100 dark:bg-gray-900"> 19+ @include('layouts.navigation') 20+ 21+ <!-- Page Heading --> 22+ @isset($header) 23+ <header class="bg-white dark:bg-gray-800 shadow"> 24+ <div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8"> 25+ {{ $header }} 26+ </div> 27+ </header> 28+ @endisset 29+ 30+ <!-- Page Content --> 31+ <main> 32+ {{ $slot }} 33+ </main> 34+ </div> 35+ </body> 36+</html> 37
/resources/views/layouts/guest.blade.php
/resources/views/layouts/guest.blade.php1+<!DOCTYPE html> 2+<html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> 3+ <head> 4+ <meta charset="utf-8"> 5+ <meta name="viewport" content="width=device-width, initial-scale=1"> 6+ <meta name="csrf-token" content="{{ csrf_token() }}"> 7+ 8+ <title>{{ config('app.name', 'Laravel') }}</title> 9+ 10+ <!-- Fonts --> 11+ <link rel="preconnect" href="https://fonts.bunny.net"> 12+ <link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" /> 13+ 14+ <!-- Scripts --> 15+ @vite(['resources/css/app.css', 'resources/js/app.js']) 16+ </head> 17+ <body class="font-sans text-gray-900 antialiased"> 18+ <div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-gray-900"> 19+ <div> 20+ <a href="/"> 21+ <x-application-logo class="w-20 h-20 fill-current text-gray-500" /> 22+ </a> 23+ </div> 24+ 25+ <div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-gray-800 shadow-md overflow-hidden sm:rounded-lg"> 26+ {{ $slot }} 27+ </div> 28+ </div> 29+ </body> 30+</html> 31
/resources/views/layouts/navigation.blade.php
/resources/views/layouts/navigation.blade.php1+<nav x-data="{ open: false }" class="bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700"> 2+ <!-- Primary Navigation Menu --> 3+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> 4+ <div class="flex justify-between h-16"> 5+ <div class="flex"> 6+ <!-- Logo --> 7+ <div class="shrink-0 flex items-center"> 8+ <a href="{{ route('dashboard') }}"> 9+ <x-application-logo class="block h-9 w-auto fill-current text-gray-800 dark:text-gray-200" /> 10+ </a> 11+ </div> 12+ 13+ <!-- Navigation Links --> 14+ <div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex"> 15+ <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')"> 16+ {{ __('Dashboard') }} 17+ </x-nav-link> 18+ </div> 19+ </div> 20+ 21+ <!-- Settings Dropdown --> 22+ <div class="hidden sm:flex sm:items-center sm:ms-6"> 23+ <x-dropdown align="right" width="48"> 24+ <x-slot name="trigger"> 25+ <button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none transition ease-in-out duration-150"> 26+ <div>{{ Auth::user()->name }}</div> 27+ 28+ <div class="ms-1"> 29+ <svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> 30+ <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" /> 31+ </svg> 32+ </div> 33+ </button> 34+ </x-slot> 35+ 36+ <x-slot name="content"> 37+ <x-dropdown-link :href="route('profile.edit')"> 38+ {{ __('Profile') }} 39+ </x-dropdown-link> 40+ 41+ <!-- Authentication --> 42+ <form method="POST" action="{{ route('logout') }}"> 43+ @csrf 44+ 45+ <x-dropdown-link :href="route('logout')" 46+ onclick="event.preventDefault(); 47+ this.closest('form').submit();"> 48+ {{ __('Log Out') }} 49+ </x-dropdown-link> 50+ </form> 51+ </x-slot> 52+ </x-dropdown> 53+ </div> 54+ 55+ <!-- Hamburger --> 56+ <div class="-me-2 flex items-center sm:hidden"> 57+ <button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-900 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-900 focus:text-gray-500 dark:focus:text-gray-400 transition duration-150 ease-in-out"> 58+ <svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24"> 59+ <path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> 60+ <path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> 61+ </svg> 62+ </button> 63+ </div> 64+ </div> 65+ </div> 66+ 67+ <!-- Responsive Navigation Menu --> 68+ <div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden"> 69+ <div class="pt-2 pb-3 space-y-1"> 70+ <x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')"> 71+ {{ __('Dashboard') }} 72+ </x-responsive-nav-link> 73+ </div> 74+ 75+ <!-- Responsive Settings Options --> 76+ <div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-600"> 77+ <div class="px-4"> 78+ <div class="font-medium text-base text-gray-800 dark:text-gray-200">{{ Auth::user()->name }}</div> 79+ <div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div> 80+ </div> 81+ 82+ <div class="mt-3 space-y-1"> 83+ <x-responsive-nav-link :href="route('profile.edit')"> 84+ {{ __('Profile') }} 85+ </x-responsive-nav-link> 86+ 87+ <!-- Authentication --> 88+ <form method="POST" action="{{ route('logout') }}"> 89+ @csrf 90+ 91+ <x-responsive-nav-link :href="route('logout')" 92+ onclick="event.preventDefault(); 93+ this.closest('form').submit();"> 94+ {{ __('Log Out') }} 95+ </x-responsive-nav-link> 96+ </form> 97+ </div> 98+ </div> 99+ </div> 100+</nav> 101
/resources/views/posts/create.blade.php
/resources/views/posts/create.blade.php1 @extends('layout') 2 3 @section('content') 4-<div class="container mt-5"> 5 <h2>新規投稿</h2> 6 <form action="{{ route('posts.store') }}" method="POST"> 7 @csrf 8 <div class="mt-3"> 9 <a href="{{ route('posts.index') }}" class="btn btn-secondary">戻る</a> 10 </div> 11-</div> 12 @endsection 13
/resources/views/posts/edit.blade.php
/resources/views/posts/edit.blade.php1 @extends('layout') 2 3 @section('content') 4-<div class="container mt-5"> 5 <h2>投稿編集</h2> 6 <form action="{{ route('posts.update', $post->id) }}" method="POST"> 7 @csrf 8 <button type="submit" class="btn btn-success">更新</button> 9 <a href="{{ route('posts.show', $post->id) }}" class="btn btn-secondary">戻る</a> 10 </form> 11-</div> 12 @endsection 13
/resources/views/posts/index.blade.php
/resources/views/posts/index.blade.php1 @extends('layout') 2 3 @section('content') 4-<div class="container mt-5"> 5- <h2>掲示板</h2> 6+ <h2>掲示板一覧</h2> 7 <a href="{{ route('posts.create') }}" class="btn btn-primary mb-3">新規投稿</a> 8 @foreach ($posts as $post) 9 <div class="card mb-3"> 10 </div> 11 </div> 12 @endforeach 13-</div> 14 @endsection 15
/resources/views/posts/show.blade.php
/resources/views/posts/show.blade.php1 @extends('layout') 2 3 @section('content') 4-<div class="container mt-5"> 5 <h2>投稿詳細</h2> 6 <div class="card mb-3"> 7 <div class="card-body"> 8 <h3 class="card-title">{{ $post->title }}</h3> 9 <p class="card-text">{{ $post->content }}</p> 10 <a href="{{ route('posts.index') }}" class="btn btn-secondary">戻る</a> 11- <a href="{{ route('posts.edit', $post->id ) }}" class="btn btn-warning">編集</a> 12- <form action="{{ route('posts.destroy', $post->id) }}" method="POST" class="d-inline"> 13- @csrf 14- @method('DELETE') 15- <button type="submit" class="btn btn-danger">削除</button> 16- </form> 17+ @if(Auth::check() && Auth::id() === $post->user_id) 18+ <a href="{{ route('posts.edit', $post->id ) }}" class="btn btn-warning">編集</a> 19+ <form action="{{ route('posts.destroy', $post->id) }}" method="POST" class="d-inline"> 20+ @csrf 21+ @method('DELETE') 22+ <button type="submit" class="btn btn-danger">削除</button> 23+ </form> 24+ @endif 25 </div> 26 </div> 27-</div> 28 @endsection 29
/resources/views/profile/edit.blade.php
/resources/views/profile/edit.blade.php1+<x-app-layout> 2+ <x-slot name="header"> 3+ <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight"> 4+ {{ __('Profile') }} 5+ </h2> 6+ </x-slot> 7+ 8+ <div class="py-12"> 9+ <div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6"> 10+ <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg"> 11+ <div class="max-w-xl"> 12+ @include('profile.partials.update-profile-information-form') 13+ </div> 14+ </div> 15+ 16+ <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg"> 17+ <div class="max-w-xl"> 18+ @include('profile.partials.update-password-form') 19+ </div> 20+ </div> 21+ 22+ <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg"> 23+ <div class="max-w-xl"> 24+ @include('profile.partials.delete-user-form') 25+ </div> 26+ </div> 27+ </div> 28+ </div> 29+</x-app-layout> 30
/resources/views/profile/partials/delete-user-form.blade.php
/resources/views/profile/partials/delete-user-form.blade.php1+<section class="space-y-6"> 2+ <header> 3+ <h2 class="text-lg font-medium text-gray-900 dark:text-gray-100"> 4+ {{ __('Delete Account') }} 5+ </h2> 6+ 7+ <p class="mt-1 text-sm text-gray-600 dark:text-gray-400"> 8+ {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }} 9+ </p> 10+ </header> 11+ 12+ <x-danger-button 13+ x-data="" 14+ x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')" 15+ >{{ __('Delete Account') }}</x-danger-button> 16+ 17+ <x-modal name="confirm-user-deletion" :show="$errors->userDeletion->isNotEmpty()" focusable> 18+ <form method="post" action="{{ route('profile.destroy') }}" class="p-6"> 19+ @csrf 20+ @method('delete') 21+ 22+ <h2 class="text-lg font-medium text-gray-900 dark:text-gray-100"> 23+ {{ __('Are you sure you want to delete your account?') }} 24+ </h2> 25+ 26+ <p class="mt-1 text-sm text-gray-600 dark:text-gray-400"> 27+ {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }} 28+ </p> 29+ 30+ <div class="mt-6"> 31+ <x-input-label for="password" value="{{ __('Password') }}" class="sr-only" /> 32+ 33+ <x-text-input 34+ id="password" 35+ name="password" 36+ type="password" 37+ class="mt-1 block w-3/4" 38+ placeholder="{{ __('Password') }}" 39+ /> 40+ 41+ <x-input-error :messages="$errors->userDeletion->get('password')" class="mt-2" /> 42+ </div> 43+ 44+ <div class="mt-6 flex justify-end"> 45+ <x-secondary-button x-on:click="$dispatch('close')"> 46+ {{ __('Cancel') }} 47+ </x-secondary-button> 48+ 49+ <x-danger-button class="ms-3"> 50+ {{ __('Delete Account') }} 51+ </x-danger-button> 52+ </div> 53+ </form> 54+ </x-modal> 55+</section> 56
/resources/views/profile/partials/update-password-form.blade.php
/resources/views/profile/partials/update-password-form.blade.php1+<section> 2+ <header> 3+ <h2 class="text-lg font-medium text-gray-900 dark:text-gray-100"> 4+ {{ __('Update Password') }} 5+ </h2> 6+ 7+ <p class="mt-1 text-sm text-gray-600 dark:text-gray-400"> 8+ {{ __('Ensure your account is using a long, random password to stay secure.') }} 9+ </p> 10+ </header> 11+ 12+ <form method="post" action="{{ route('password.update') }}" class="mt-6 space-y-6"> 13+ @csrf 14+ @method('put') 15+ 16+ <div> 17+ <x-input-label for="update_password_current_password" :value="__('Current Password')" /> 18+ <x-text-input id="update_password_current_password" name="current_password" type="password" class="mt-1 block w-full" autocomplete="current-password" /> 19+ <x-input-error :messages="$errors->updatePassword->get('current_password')" class="mt-2" /> 20+ </div> 21+ 22+ <div> 23+ <x-input-label for="update_password_password" :value="__('New Password')" /> 24+ <x-text-input id="update_password_password" name="password" type="password" class="mt-1 block w-full" autocomplete="new-password" /> 25+ <x-input-error :messages="$errors->updatePassword->get('password')" class="mt-2" /> 26+ </div> 27+ 28+ <div> 29+ <x-input-label for="update_password_password_confirmation" :value="__('Confirm Password')" /> 30+ <x-text-input id="update_password_password_confirmation" name="password_confirmation" type="password" class="mt-1 block w-full" autocomplete="new-password" /> 31+ <x-input-error :messages="$errors->updatePassword->get('password_confirmation')" class="mt-2" /> 32+ </div> 33+ 34+ <div class="flex items-center gap-4"> 35+ <x-primary-button>{{ __('Save') }}</x-primary-button> 36+ 37+ @if (session('status') === 'password-updated') 38+ <p 39+ x-data="{ show: true }" 40+ x-show="show" 41+ x-transition 42+ x-init="setTimeout(() => show = false, 2000)" 43+ class="text-sm text-gray-600 dark:text-gray-400" 44+ >{{ __('Saved.') }}</p> 45+ @endif 46+ </div> 47+ </form> 48+</section> 49
/resources/views/profile/partials/update-profile-information-form.blade.php
/resources/views/profile/partials/update-profile-information-form.blade.php1+<section> 2+ <header> 3+ <h2 class="text-lg font-medium text-gray-900 dark:text-gray-100"> 4+ {{ __('Profile Information') }} 5+ </h2> 6+ 7+ <p class="mt-1 text-sm text-gray-600 dark:text-gray-400"> 8+ {{ __("Update your account's profile information and email address.") }} 9+ </p> 10+ </header> 11+ 12+ <form id="send-verification" method="post" action="{{ route('verification.send') }}"> 13+ @csrf 14+ </form> 15+ 16+ <form method="post" action="{{ route('profile.update') }}" class="mt-6 space-y-6"> 17+ @csrf 18+ @method('patch') 19+ 20+ <div> 21+ <x-input-label for="name" :value="__('Name')" /> 22+ <x-text-input id="name" name="name" type="text" class="mt-1 block w-full" :value="old('name', $user->name)" required autofocus autocomplete="name" /> 23+ <x-input-error class="mt-2" :messages="$errors->get('name')" /> 24+ </div> 25+ 26+ <div> 27+ <x-input-label for="email" :value="__('Email')" /> 28+ <x-text-input id="email" name="email" type="email" class="mt-1 block w-full" :value="old('email', $user->email)" required autocomplete="username" /> 29+ <x-input-error class="mt-2" :messages="$errors->get('email')" /> 30+ 31+ @if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->hasVerifiedEmail()) 32+ <div> 33+ <p class="text-sm mt-2 text-gray-800 dark:text-gray-200"> 34+ {{ __('Your email address is unverified.') }} 35+ 36+ <button form="send-verification" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800"> 37+ {{ __('Click here to re-send the verification email.') }} 38+ </button> 39+ </p> 40+ 41+ @if (session('status') === 'verification-link-sent') 42+ <p class="mt-2 font-medium text-sm text-green-600 dark:text-green-400"> 43+ {{ __('A new verification link has been sent to your email address.') }} 44+ </p> 45+ @endif 46+ </div> 47+ @endif 48+ </div> 49+ 50+ <div class="flex items-center gap-4"> 51+ <x-primary-button>{{ __('Save') }}</x-primary-button> 52+ 53+ @if (session('status') === 'profile-updated') 54+ <p 55+ x-data="{ show: true }" 56+ x-show="show" 57+ x-transition 58+ x-init="setTimeout(() => show = false, 2000)" 59+ class="text-sm text-gray-600 dark:text-gray-400" 60+ >{{ __('Saved.') }}</p> 61+ @endif 62+ </div> 63+ </form> 64+</section> 65
/routes/auth.php
/routes/auth.php1+<?php 2+ 3+use App\Http\Controllers\Auth\AuthenticatedSessionController; 4+use App\Http\Controllers\Auth\ConfirmablePasswordController; 5+use App\Http\Controllers\Auth\EmailVerificationNotificationController; 6+use App\Http\Controllers\Auth\EmailVerificationPromptController; 7+use App\Http\Controllers\Auth\NewPasswordController; 8+use App\Http\Controllers\Auth\PasswordController; 9+use App\Http\Controllers\Auth\PasswordResetLinkController; 10+use App\Http\Controllers\Auth\RegisteredUserController; 11+use App\Http\Controllers\Auth\VerifyEmailController; 12+use Illuminate\Support\Facades\Route; 13+ 14+Route::middleware('guest')->group(function () { 15+ Route::get('register', [RegisteredUserController::class, 'create']) 16+ ->name('register'); 17+ 18+ Route::post('register', [RegisteredUserController::class, 'store']); 19+ 20+ Route::get('login', [AuthenticatedSessionController::class, 'create']) 21+ ->name('login'); 22+ 23+ Route::post('login', [AuthenticatedSessionController::class, 'store']); 24+ 25+ Route::get('forgot-password', [PasswordResetLinkController::class, 'create']) 26+ ->name('password.request'); 27+ 28+ Route::post('forgot-password', [PasswordResetLinkController::class, 'store']) 29+ ->name('password.email'); 30+ 31+ Route::get('reset-password/{token}', [NewPasswordController::class, 'create']) 32+ ->name('password.reset'); 33+ 34+ Route::post('reset-password', [NewPasswordController::class, 'store']) 35+ ->name('password.store'); 36+}); 37+ 38+Route::middleware('auth')->group(function () { 39+ Route::get('verify-email', EmailVerificationPromptController::class) 40+ ->name('verification.notice'); 41+ 42+ Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) 43+ ->middleware(['signed', 'throttle:6,1']) 44+ ->name('verification.verify'); 45+ 46+ Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) 47+ ->middleware('throttle:6,1') 48+ ->name('verification.send'); 49+ 50+ Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) 51+ ->name('password.confirm'); 52+ 53+ Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); 54+ 55+ Route::put('password', [PasswordController::class, 'update'])->name('password.update'); 56+ 57+ Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) 58+ ->name('logout'); 59+}); 60
/routes/web.php
/routes/web.php1 <?php 2 3-use Illuminate\Support\Facades\Route; 4+use App\Http\Controllers\ProfileController; 5 use App\Http\Controllers\PostController; 6+use Illuminate\Support\Facades\Route; 7 8 Route::get('/', function () { 9 return view('welcome'); 10 }); 11 12-Route::resource('posts', PostController::class); 13+Route::get('/dashboard', function () { 14+ return view('dashboard'); 15+})->middleware(['auth', 'verified'])->name('dashboard'); 16+ 17+Route::middleware('auth')->group(function () { 18+ Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); 19+ Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); 20+ Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); 21+}); 22+ 23+// ログイン必須の新規投稿と保存のルート設定 24+Route::middleware(['auth'])->group(function () { 25+ Route::get('/posts/create', [PostController::class, 'create'])->name('posts.create'); 26+ Route::post('/posts', [PostController::class, 'store'])->name('posts.store'); 27+}); 28+ 29+// 未ログインでもアクセス可能な一覧と詳細ページ 30+Route::resource('posts', PostController::class)->only(['index', 'show']); 31+ 32+// 編集・削除はログインが必要だが、権限はポリシーで制限 33+Route::resource('posts', PostController::class)->except(['create', 'store', 'index', 'show'])->middleware('auth'); 34+ 35+require __DIR__.'/auth.php'; 36
/tailwind.config.js
/tailwind.config.js1 import defaultTheme from 'tailwindcss/defaultTheme'; 2+import forms from '@tailwindcss/forms'; 3 4 /** @type {import('tailwindcss').Config} */ 5 export default { 6 content: [ 7 './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', 8 './storage/framework/views/*.php', 9- './resources/**/*.blade.php', 10- './resources/**/*.js', 11- './resources/**/*.vue', 12+ './resources/views/**/*.blade.php', 13 ], 14+ 15 theme: { 16 extend: { 17 fontFamily: { 18 }, 19 }, 20 }, 21- plugins: [], 22+ 23+ plugins: [forms], 24 }; 25
/tests/Feature/Auth/AuthenticationTest.php
/tests/Feature/Auth/AuthenticationTest.php1+<?php 2+ 3+namespace Tests\Feature\Auth; 4+ 5+use App\Models\User; 6+use Illuminate\Foundation\Testing\RefreshDatabase; 7+use Tests\TestCase; 8+ 9+class AuthenticationTest extends TestCase 10+{ 11+ use RefreshDatabase; 12+ 13+ public function test_login_screen_can_be_rendered(): void 14+ { 15+ $response = $this->get('/login'); 16+ 17+ $response->assertStatus(200); 18+ } 19+ 20+ public function test_users_can_authenticate_using_the_login_screen(): void 21+ { 22+ $user = User::factory()->create(); 23+ 24+ $response = $this->post('/login', [ 25+ 'email' => $user->email, 26+ 'password' => 'password', 27+ ]); 28+ 29+ $this->assertAuthenticated(); 30+ $response->assertRedirect(route('dashboard', absolute: false)); 31+ } 32+ 33+ public function test_users_can_not_authenticate_with_invalid_password(): void 34+ { 35+ $user = User::factory()->create(); 36+ 37+ $this->post('/login', [ 38+ 'email' => $user->email, 39+ 'password' => 'wrong-password', 40+ ]); 41+ 42+ $this->assertGuest(); 43+ } 44+ 45+ public function test_users_can_logout(): void 46+ { 47+ $user = User::factory()->create(); 48+ 49+ $response = $this->actingAs($user)->post('/logout'); 50+ 51+ $this->assertGuest(); 52+ $response->assertRedirect('/'); 53+ } 54+} 55
/tests/Feature/Auth/EmailVerificationTest.php
/tests/Feature/Auth/EmailVerificationTest.php1+<?php 2+ 3+namespace Tests\Feature\Auth; 4+ 5+use App\Models\User; 6+use Illuminate\Auth\Events\Verified; 7+use Illuminate\Foundation\Testing\RefreshDatabase; 8+use Illuminate\Support\Facades\Event; 9+use Illuminate\Support\Facades\URL; 10+use Tests\TestCase; 11+ 12+class EmailVerificationTest extends TestCase 13+{ 14+ use RefreshDatabase; 15+ 16+ public function test_email_verification_screen_can_be_rendered(): void 17+ { 18+ $user = User::factory()->unverified()->create(); 19+ 20+ $response = $this->actingAs($user)->get('/verify-email'); 21+ 22+ $response->assertStatus(200); 23+ } 24+ 25+ public function test_email_can_be_verified(): void 26+ { 27+ $user = User::factory()->unverified()->create(); 28+ 29+ Event::fake(); 30+ 31+ $verificationUrl = URL::temporarySignedRoute( 32+ 'verification.verify', 33+ now()->addMinutes(60), 34+ ['id' => $user->id, 'hash' => sha1($user->email)] 35+ ); 36+ 37+ $response = $this->actingAs($user)->get($verificationUrl); 38+ 39+ Event::assertDispatched(Verified::class); 40+ $this->assertTrue($user->fresh()->hasVerifiedEmail()); 41+ $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); 42+ } 43+ 44+ public function test_email_is_not_verified_with_invalid_hash(): void 45+ { 46+ $user = User::factory()->unverified()->create(); 47+ 48+ $verificationUrl = URL::temporarySignedRoute( 49+ 'verification.verify', 50+ now()->addMinutes(60), 51+ ['id' => $user->id, 'hash' => sha1('wrong-email')] 52+ ); 53+ 54+ $this->actingAs($user)->get($verificationUrl); 55+ 56+ $this->assertFalse($user->fresh()->hasVerifiedEmail()); 57+ } 58+} 59
/tests/Feature/Auth/PasswordConfirmationTest.php
/tests/Feature/Auth/PasswordConfirmationTest.php1+<?php 2+ 3+namespace Tests\Feature\Auth; 4+ 5+use App\Models\User; 6+use Illuminate\Foundation\Testing\RefreshDatabase; 7+use Tests\TestCase; 8+ 9+class PasswordConfirmationTest extends TestCase 10+{ 11+ use RefreshDatabase; 12+ 13+ public function test_confirm_password_screen_can_be_rendered(): void 14+ { 15+ $user = User::factory()->create(); 16+ 17+ $response = $this->actingAs($user)->get('/confirm-password'); 18+ 19+ $response->assertStatus(200); 20+ } 21+ 22+ public function test_password_can_be_confirmed(): void 23+ { 24+ $user = User::factory()->create(); 25+ 26+ $response = $this->actingAs($user)->post('/confirm-password', [ 27+ 'password' => 'password', 28+ ]); 29+ 30+ $response->assertRedirect(); 31+ $response->assertSessionHasNoErrors(); 32+ } 33+ 34+ public function test_password_is_not_confirmed_with_invalid_password(): void 35+ { 36+ $user = User::factory()->create(); 37+ 38+ $response = $this->actingAs($user)->post('/confirm-password', [ 39+ 'password' => 'wrong-password', 40+ ]); 41+ 42+ $response->assertSessionHasErrors(); 43+ } 44+} 45
/tests/Feature/Auth/PasswordResetTest.php
/tests/Feature/Auth/PasswordResetTest.php1+<?php 2+ 3+namespace Tests\Feature\Auth; 4+ 5+use App\Models\User; 6+use Illuminate\Auth\Notifications\ResetPassword; 7+use Illuminate\Foundation\Testing\RefreshDatabase; 8+use Illuminate\Support\Facades\Notification; 9+use Tests\TestCase; 10+ 11+class PasswordResetTest extends TestCase 12+{ 13+ use RefreshDatabase; 14+ 15+ public function test_reset_password_link_screen_can_be_rendered(): void 16+ { 17+ $response = $this->get('/forgot-password'); 18+ 19+ $response->assertStatus(200); 20+ } 21+ 22+ public function test_reset_password_link_can_be_requested(): void 23+ { 24+ Notification::fake(); 25+ 26+ $user = User::factory()->create(); 27+ 28+ $this->post('/forgot-password', ['email' => $user->email]); 29+ 30+ Notification::assertSentTo($user, ResetPassword::class); 31+ } 32+ 33+ public function test_reset_password_screen_can_be_rendered(): void 34+ { 35+ Notification::fake(); 36+ 37+ $user = User::factory()->create(); 38+ 39+ $this->post('/forgot-password', ['email' => $user->email]); 40+ 41+ Notification::assertSentTo($user, ResetPassword::class, function ($notification) { 42+ $response = $this->get('/reset-password/'.$notification->token); 43+ 44+ $response->assertStatus(200); 45+ 46+ return true; 47+ }); 48+ } 49+ 50+ public function test_password_can_be_reset_with_valid_token(): void 51+ { 52+ Notification::fake(); 53+ 54+ $user = User::factory()->create(); 55+ 56+ $this->post('/forgot-password', ['email' => $user->email]); 57+ 58+ Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { 59+ $response = $this->post('/reset-password', [ 60+ 'token' => $notification->token, 61+ 'email' => $user->email, 62+ 'password' => 'password', 63+ 'password_confirmation' => 'password', 64+ ]); 65+ 66+ $response 67+ ->assertSessionHasNoErrors() 68+ ->assertRedirect(route('login')); 69+ 70+ return true; 71+ }); 72+ } 73+} 74
/tests/Feature/Auth/PasswordUpdateTest.php
/tests/Feature/Auth/PasswordUpdateTest.php1+<?php 2+ 3+namespace Tests\Feature\Auth; 4+ 5+use App\Models\User; 6+use Illuminate\Foundation\Testing\RefreshDatabase; 7+use Illuminate\Support\Facades\Hash; 8+use Tests\TestCase; 9+ 10+class PasswordUpdateTest extends TestCase 11+{ 12+ use RefreshDatabase; 13+ 14+ public function test_password_can_be_updated(): void 15+ { 16+ $user = User::factory()->create(); 17+ 18+ $response = $this 19+ ->actingAs($user) 20+ ->from('/profile') 21+ ->put('/password', [ 22+ 'current_password' => 'password', 23+ 'password' => 'new-password', 24+ 'password_confirmation' => 'new-password', 25+ ]); 26+ 27+ $response 28+ ->assertSessionHasNoErrors() 29+ ->assertRedirect('/profile'); 30+ 31+ $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); 32+ } 33+ 34+ public function test_correct_password_must_be_provided_to_update_password(): void 35+ { 36+ $user = User::factory()->create(); 37+ 38+ $response = $this 39+ ->actingAs($user) 40+ ->from('/profile') 41+ ->put('/password', [ 42+ 'current_password' => 'wrong-password', 43+ 'password' => 'new-password', 44+ 'password_confirmation' => 'new-password', 45+ ]); 46+ 47+ $response 48+ ->assertSessionHasErrorsIn('updatePassword', 'current_password') 49+ ->assertRedirect('/profile'); 50+ } 51+} 52
/tests/Feature/Auth/RegistrationTest.php
/tests/Feature/Auth/RegistrationTest.php1+<?php 2+ 3+namespace Tests\Feature\Auth; 4+ 5+use Illuminate\Foundation\Testing\RefreshDatabase; 6+use Tests\TestCase; 7+ 8+class RegistrationTest extends TestCase 9+{ 10+ use RefreshDatabase; 11+ 12+ public function test_registration_screen_can_be_rendered(): void 13+ { 14+ $response = $this->get('/register'); 15+ 16+ $response->assertStatus(200); 17+ } 18+ 19+ public function test_new_users_can_register(): void 20+ { 21+ $response = $this->post('/register', [ 22+ 'name' => 'Test User', 23+ 'email' => 'test@example.com', 24+ 'password' => 'password', 25+ 'password_confirmation' => 'password', 26+ ]); 27+ 28+ $this->assertAuthenticated(); 29+ $response->assertRedirect(route('dashboard', absolute: false)); 30+ } 31+} 32
/tests/Feature/ProfileTest.php
/tests/Feature/ProfileTest.php1+<?php 2+ 3+namespace Tests\Feature; 4+ 5+use App\Models\User; 6+use Illuminate\Foundation\Testing\RefreshDatabase; 7+use Tests\TestCase; 8+ 9+class ProfileTest extends TestCase 10+{ 11+ use RefreshDatabase; 12+ 13+ public function test_profile_page_is_displayed(): void 14+ { 15+ $user = User::factory()->create(); 16+ 17+ $response = $this 18+ ->actingAs($user) 19+ ->get('/profile'); 20+ 21+ $response->assertOk(); 22+ } 23+ 24+ public function test_profile_information_can_be_updated(): void 25+ { 26+ $user = User::factory()->create(); 27+ 28+ $response = $this 29+ ->actingAs($user) 30+ ->patch('/profile', [ 31+ 'name' => 'Test User', 32+ 'email' => 'test@example.com', 33+ ]); 34+ 35+ $response 36+ ->assertSessionHasNoErrors() 37+ ->assertRedirect('/profile'); 38+ 39+ $user->refresh(); 40+ 41+ $this->assertSame('Test User', $user->name); 42+ $this->assertSame('test@example.com', $user->email); 43+ $this->assertNull($user->email_verified_at); 44+ } 45+ 46+ public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged(): void 47+ { 48+ $user = User::factory()->create(); 49+ 50+ $response = $this 51+ ->actingAs($user) 52+ ->patch('/profile', [ 53+ 'name' => 'Test User', 54+ 'email' => $user->email, 55+ ]); 56+ 57+ $response 58+ ->assertSessionHasNoErrors() 59+ ->assertRedirect('/profile'); 60+ 61+ $this->assertNotNull($user->refresh()->email_verified_at); 62+ } 63+ 64+ public function test_user_can_delete_their_account(): void 65+ { 66+ $user = User::factory()->create(); 67+ 68+ $response = $this 69+ ->actingAs($user) 70+ ->delete('/profile', [ 71+ 'password' => 'password', 72+ ]); 73+ 74+ $response 75+ ->assertSessionHasNoErrors() 76+ ->assertRedirect('/'); 77+ 78+ $this->assertGuest(); 79+ $this->assertNull($user->fresh()); 80+ } 81+ 82+ public function test_correct_password_must_be_provided_to_delete_account(): void 83+ { 84+ $user = User::factory()->create(); 85+ 86+ $response = $this 87+ ->actingAs($user) 88+ ->from('/profile') 89+ ->delete('/profile', [ 90+ 'password' => 'wrong-password', 91+ ]); 92+ 93+ $response 94+ ->assertSessionHasErrorsIn('userDeletion', 'password') 95+ ->assertRedirect('/profile'); 96+ 97+ $this->assertNotNull($user->fresh()); 98+ } 99+} 100
/vite.config.js
/vite.config.js1 export default defineConfig({ 2 plugins: [ 3 laravel({ 4- input: ['resources/css/app.css', 'resources/js/app.js'], 5+ input: [ 6+ 'resources/css/app.css', 7+ 'resources/js/app.js', 8+ ], 9 refresh: true, 10 }), 11 ], 12
コード解説
変更点: 認証パッケージLaravel Breezeの追加
/composer.json1 }, 2 "require-dev": { 3 "fakerphp/faker": "^1.23", 4+ "laravel/breeze": "^2.2", 5 "laravel/pail": "^1.1", 6 "laravel/pint": "^1.13", 7 "laravel/sail": "^1.26", 8
Laravelプロジェクトに、ログイン認証機能一式を簡単に追加できるパッケージ「Laravel Breeze」をインストールします。composer.jsonは、プロジェクトが利用するPHPパッケージを管理するファイルです。ここにlaravel/breezeを追記し、インストールすることで、認証機能に必要なサーバー側のプログラムが使えるようになります。
変更点: フロントエンド開発環境のセットアップ
/package.json1 "devDependencies": { 2- "autoprefixer": "^10.4.20", 3+ "@tailwindcss/forms": "^0.5.2", 4+ "alpinejs": "^3.4.2", 5+ "autoprefixer": "^10.4.2", 6 "axios": "^1.7.4", 7 "concurrently": "^9.0.1", 8 "laravel-vite-plugin": "^1.0", 9- "postcss": "^8.4.47", 10- "tailwindcss": "^3.4.13", 11+ "postcss": "^8.4.31", 12+ "tailwindcss": "^3.1.0", 13 "vite": "^5.0" 14 } 15 }
package.jsonは、JavaScript関連のパッケージを管理するファイルです。Laravel Breezeは、認証画面の見た目や動きを整えるために、@tailwindcss/forms(Tailwind CSS用のフォームスタイル)やalpinejs(シンプルなJavaScriptフレームワーク)といったライブラリを使用します。これらのライブラリをプロジェクトに追加しています。
変更点: Alpine.jsの初期化
/resources/js/app.js1 import './bootstrap'; 2+ 3+import Alpine from 'alpinejs'; 4+ 5+window.Alpine = Alpine; 6+ 7+Alpine.start();
Breezeが提供する認証画面などのドロップダウンメニューといった動的な部分を機能させるために、JavaScriptフレームワークのAlpine.jsを読み込んで初期化しています。これにより、Webページ上でAlpine.jsが使えるようになります。
変更点: Tailwind CSSの設定更新
/tailwind.config.js1 import defaultTheme from 'tailwindcss/defaultTheme'; 2+import forms from '@tailwindcss/forms'; 3 4 /** @type {import('tailwindcss').Config} */ 5 export default { 6 content: [ 7 './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', 8 './storage/framework/views/*.php', 9- './resources/**/*.blade.php', 10- './resources/**/*.js', 11- './resources/**/*.vue', 12+ './resources/views/**/*.blade.php', 13 ], 14+ 15 theme: { 16 extend: { 17 fontFamily: { 18 }, 19 }, 20 }, 21- plugins: [], 22+ 23+ plugins: [forms], 24 };
CSSフレームワークであるTailwind CSSの設定ファイルです。Breezeのフォーム部品にきれいなスタイルを適用するため、プラグインとして@tailwindcss/formsを読み込んでいます。また、contentのパスを更新し、Tailwind CSSが適用されるファイルを正しく指定しています。
変更点: 投稿(posts)テーブルとユーザー(users)テーブルの関連付け
/database/migrations/2024_10_26_120312_add_user_id_to_posts_table.php1+<?php 2+ 3+use Illuminate\Database\Migrations\Migration; 4+use Illuminate\Database\Schema\Blueprint; 5+use Illuminate\Support\Facades\Schema; 6+ 7+return new class extends Migration 8+{ 9+ /** 10+ * Run the migrations. 11+ */ 12+ public function up(): void 13+ { 14+ Schema::table('posts', function (Blueprint $table) { 15+ $table->foreignId('user_id')->nullable()->constrained()->onDelete('cascade'); 16+ }); 17+ } 18+ 19+ /** 20+ * Reverse the migrations. 21+ */ 22+ public function down(): void 23+ { 24+ Schema::table('posts', function (Blueprint $table) { 25+ $table->dropForeign(['user_id']); 26+ $table->dropColumn('user_id'); 27+ }); 28+ } 29+};
データベースの設計図であるマイグレーションファイルを追加し、postsテーブルにuser_idという新しいカラムを追加します。これは、「どのユーザーがその投稿をしたのか」を記録するための重要なカラムです。constrained()はusersテーブルのidと関連付ける設定で、onDelete('cascade')はユーザーが退会などで削除された場合に、そのユーザーの投稿も一緒に削除する設定です。
変更点: Postモデルにuser_idを追記
/app/Models/Post.php1 { 2 protected $fillable = [ 3 'title', 4- 'content' 5+ 'content', 6+ 'user_id' 7 ]; 8 }
Postモデルは、postsテーブルのデータを操作するためのプログラムです。$fillableプロパティにuser_idを追加することで、プログラムからuser_idを含んだ投稿データを一度に作成・更新できるようになります。これはセキュリティ対策の一環で、意図しないカラムが更新されるのを防ぎます。
変更点: 認証関連のルート設定を追加
/routes/web.php1-use Illuminate\Support\Facades\Route; 2+use App\Http\Controllers\ProfileController; 3 use App\Http\Controllers\PostController; 4+use Illuminate\Support\Facades\Route; 5 6-Route::resource('posts', PostController::class); 7+Route::get('/dashboard', function () { 8+ return view('dashboard'); 9+})->middleware(['auth', 'verified'])->name('dashboard'); 10+ 11+Route::middleware('auth')->group(function () { 12+ Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); 13+ Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); 14+ Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); 15+}); 16+ 17+// ログイン必須の新規投稿と保存のルート設定 18+Route::middleware(['auth'])->group(function () { 19+ Route::get('/posts/create', [PostController::class, 'create'])->name('posts.create'); 20+ Route::post('/posts', [PostController::class, 'store'])->name('posts.store'); 21+}); 22+ 23+// 未ログインでもアクセス可能な一覧と詳細ページ 24+Route::resource('posts', PostController::class)->only(['index', 'show']); 25+ 26+// 編集・削除はログインが必要だが、権限はポリシーで制限 27+Route::resource('posts', PostController::class)->except(['create', 'store', 'index', 'show'])->middleware('auth'); 28+ 29+require __DIR__.'/auth.php';
WebアプリケーションのURLとプログラムの処理を結びつけるルート設定ファイルです。Breezeによって、ログイン後のダッシュボードやプロフィール編集画面のルートが追加されました。 また、掲示板機能のルートを以下のように整理しています。
middleware('auth')を使い、投稿の作成・保存・編集・更新・削除はログインしているユーザーしかアクセスできないように制限しています。- 一方で、投稿の一覧表示(
index)と詳細表示(show)は誰でも見られるように、制限をかけていません。 - 最後に
require __DIR__.'/auth.php';で、Breezeが生成した認証関連のルート設定(ログイン、ログアウト、ユーザー登録など)を読み込んでいます。
変更点: 認証関連のルーティングファイルを生成
/routes/auth.php1+<?php 2+ 3+use App\Http\Controllers\Auth\AuthenticatedSessionController; 4+// ... (use statements) 5+use Illuminate\Support\Facades\Route; 6+ 7+Route::middleware('guest')->group(function () { 8+ // ログイン、ユーザー登録、パスワードリセットなど 9+}); 10+ 11+Route::middleware('auth')->group(function () { 12+ // メール認証、パスワード確認、ログアウトなど 13+});
Breezeをインストールすると、認証機能専用のルート設定ファイルauth.phpが作成されます。ここには、ログイン、ログアウト、ユーザー登録、パスワードリセットなど、認証に関するすべてのルートがまとめられています。guestミドルウェアは未ログインユーザーのみ、authミドルウェアはログイン済みユーザーのみがアクセスできるルートを定義しています。
変更点: 新規登録後のリダイレクト先を変更
/app/Http/Controllers/Auth/RegisteredUserController.php1 Auth::login($user); 2 3+ // 新規登録後にリダイレクトするURLを指定 4+ return redirect()->route('posts.index'); 5+ 6 // return redirect(route('dashboard', absolute: false)); 7 } 8 }
ユーザーの新規登録処理を担当するコントローラです。デフォルトでは、新規登録が完了するとダッシュボード画面に遷移しますが、今回は掲示板アプリなので、投稿一覧ページ(posts.index)に遷移するようにリダイレクト先を変更しています。
変更点: ログイン後のリダイレクト先を変更
/app/Http/Controllers/Auth/AuthenticatedSessionController.php1 $request->session()->regenerate(); 2 3+ // Login後のリダイレクト先をposts.indexに変更 4+ return redirect()->route('posts.index'); 5+ 6 // return redirect()->intended(route('dashboard', absolute: false)); 7 } 8 }
ログイン処理を担当するコントローラです。こちらも新規登録と同様に、ログインが成功した後の画面遷移先を、デフォルトのダッシュボードから投稿一覧ページ(posts.index)に変更しています。これにより、ユーザーはログイン後すぐに掲示板を閲覧できます。
変更点: 投稿機能にアクセス制御を実装
/app/Http/Controllers/PostController.php1 2 use Illuminate\Http\Request; 3 use App\Models\Post; 4+use Illuminate\Support\Facades\Auth; 5 6 class PostController extends Controller 7 { 8 */ 9 public function create() 10 { 11+ // ログインユーザーのみが新規作成ページにアクセス可能 12+ if (!Auth::check()) { 13+ return redirect()->route('login'); 14+ } 15+ 16 return view('posts.create'); 17 } 18 19 */ 20 public function store(Request $request) 21 { 22+ // ログインユーザーのみが投稿可能 23+ if (!Auth::check()) { 24+ return redirect()->route('login'); 25+ } 26+ 27 $request->validate([ 28 'title' => 'required|max:255', 29 'content' => 'required' 30 ]); 31 32- $post = Post::create($request->all()); 33+ $post = Post::create([ 34+ 'title' => $request->title, 35+ 'content' => $request->content, 36+ 'user_id' => auth()->id() 37+ ]); 38 39 return redirect()->route('posts.show', ['post' => $post->id]); 40 } 41 42 public function edit(string $id) 43 { 44 $post = Post::findOrFail($id); 45+ 46+ // 所有者のみ編集ページにアクセス可能 47+ if (Auth::id() !== $post->user_id) { 48+ return redirect()->route('posts.index'); 49+ } 50+ 51 return view('posts.edit', compact('post')); 52 } 53 54 public function update(Request $request, string $id) 55 { 56 $post = Post::findOrFail($id); 57 58+ // 所有者のみ更新可能 59+ if (Auth::id() !== $post->user_id) { 60+ return redirect()->route('posts.index'); 61+ } 62+ 63 $post->update([ 64 'title' => $request->title, 65 'content' => $request->content 66 ]); 67 } 68 69 public function destroy(string $id) 70 { 71 $post = Post::findOrFail($id); 72+ 73+ // 所有者のみ削除可能 74+ if (Auth::id() !== $post->user_id) { 75+ return redirect()->route('posts.index'); 76+ } 77+ 78 $post->delete(); 79+ 80 return redirect()->route('posts.index'); 81 } 82 }
投稿のCRUD(作成、読み取り、更新、削除)処理を行うコントローラに、認証に基づいたアクセス制御を追加しました。
- 投稿作成(create, store):
Auth::check()を使い、ログインしていないユーザーがアクセスしようとした場合は、ログインページにリダイレクトさせます。 - 投稿保存(store): 投稿データをデータベースに保存する際、
'user_id' => auth()->id()というコードで、現在ログインしているユーザーのIDをuser_idとして保存します。これにより、投稿と作成者を紐付けます。 - 編集・更新・削除(edit, update, destroy):
Auth::id() !== $post->user_idという条件式で、ログインしているユーザーのIDと、投稿のuser_idを比較します。もし一致しない場合(つまり、他人の投稿を操作しようとした場合)、投稿一覧ページにリダイレクトさせ、不正な操作を防ぎます。これはアプリケーションのセキュリティを保つ上で非常に重要な処理です。
変更点: 共通レイアウトにナビゲーションバーを追加
/resources/views/layout.blade.php1+ <nav class="navbar navbar-expand-lg bg-primary"> 2+ <div class="container"> 3+ <a class="navbar-brand text-white" href="{{ route('posts.index') }}">掲示板アプリ</a> 4+ <div class="ml-auto"> 5+ <ul class="navbar-nav"> 6+ @if (Auth::check()) 7+ <li class="nav-item"> 8+ <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;"> 9+ @csrf 10+ </form> 11+ <a class="nav-link text-white" href="#" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">ログアウト</a> 12+ </li> 13+ @else 14+ <li class="nav-item"> 15+ <a class="nav-link text-white" href="{{ route('login') }}">ログイン</a> 16+ </li> 17+ <li class="nav-item"> 18+ <a class="nav-link text-white" href="{{ route('register') }}">新規登録</a> 19+ </li> 20+ @endif 21+ </ul> 22+ </div> 23+ </div> 24+ </nav> 25+ <div class="container mt-4"> 26+ @yield('content') 27+ </div>
すべてのページで共通して使われるレイアウトファイルです。ページ上部にナビゲーションバーを追加しました。@if (Auth::check())というBladeディレクティブを使い、ユーザーのログイン状態に応じて表示を切り替えています。
- ログインしている場合(
Auth::check()がtrue): 「ログアウト」リンクを表示します。 - ログインしていない場合(
Auth::check()がfalse): 「ログイン」と「新規登録」のリンクを表示します。
変更点: 投稿詳細ページに本人限定の操作ボタンを表示
/resources/views/posts/show.blade.php1 <p class="card-text">{{ $post->content }}</p> 2 <a href="{{ route('posts.index') }}" class="btn btn-secondary">戻る</a> 3- <a href="{{ route('posts.edit', $post->id ) }}" class="btn btn-warning">編集</a> 4- <form action="{{ route('posts.destroy', $post->id) }}" method="POST" class="d-inline"> 5- @csrf 6- @method('DELETE') 7- <button type="submit" class="btn btn-danger">削除</button> 8- </form> 9+ @if(Auth::check() && Auth::id() === $post->user_id) 10+ <a href="{{ route('posts.edit', $post->id ) }}" class="btn btn-warning">編集</a> 11+ <form action="{{ route('posts.destroy', $post->id) }}" method="POST" class="d-inline"> 12+ @csrf 13+ @method('DELETE') 14+ <button type="submit" class="btn btn-danger">削除</button> 15+ </form> 16+ @endif 17 </div> 18 </div> 19-</div>
投稿の詳細ページです。ここでも@ifディレクティブを使い、Auth::check()(ログインしているか)とAuth::id() === $post->user_id(自分の投稿か)の両方の条件を満たす場合にのみ、「編集」ボタンと「削除」ボタンを表示するように変更しました。コントローラでのアクセス制御と合わせて、UI上でも本人以外のユーザーには操作ボタンを見せないようにすることで、より親切な作りになります。
変更点: Breezeによる認証関連の画面ファイル追加
/resources/views/auth/login.blade.php1+<x-guest-layout> 2+ <!-- Session Status --> 3+ <x-auth-session-status class="mb-4" :status="session('status')" /> 4+ 5+ <form method="POST" action="{{ route('login') }}"> 6+ @csrf 7+ 8+ <!-- Email Address --> 9+ <div> 10+ <x-input-label for="email" :value="__('Email')" /> 11+ <x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" /> 12+ <x-input-error :messages="$errors->get('email')" class="mt-2" /> 13+ </div> 14+ 15+ <!-- ... (中略) ... --> 16+ 17+ <x-primary-button class="ms-3"> 18+ {{ __('Log in') }} 19+ </x-primary-button> 20+ </div> 21+ </form> 22+</x-guest-layout>
Breezeをインストールすると、ログイン、ユーザー登録、パスワードリセットなどの認証機能に必要な画面(ビューファイル)が一式、resources/views/authディレクトリ内に生成されます。この差分はログイン画面のコードです。x-guest-layoutやx-text-inputのように、<x-...>という形式で書かれているのは、再利用可能なUI部品(Bladeコンポーネント)を呼び出している記述です。これにより、統一感のあるUIを効率的に作成できます。同様のファイルが多数追加されています。
おわりに
今回はLaravel Breezeを使い、掲示板アプリに本格的なログイン認証機能を実装しました。Breezeを導入するだけで、認証に必要なコントローラやビュー、ルート設定などが自動で生成される手軽さを体験できました。さらに、PostControllerにログインユーザーのID(Auth::id())と投稿のuser_idを比較する処理を加え、投稿者本人だけが編集・削除できるアクセス制御を実現しました。認証機能をただ追加するだけでなく、既存の機能と連携させてセキュリティを向上させる具体的な方法を学べたことが、今回の大きな収穫です。