diff --git a/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/app/Http/Controllers/Auth/AuthenticatedSessionController.php
new file mode 100644
index 0000000..494a106
--- /dev/null
+++ b/app/Http/Controllers/Auth/AuthenticatedSessionController.php
@@ -0,0 +1,48 @@
+authenticate();
+
+ $request->session()->regenerate();
+
+ return redirect()->intended(RouteServiceProvider::HOME);
+ }
+
+ /**
+ * Destroy an authenticated session.
+ */
+ public function destroy(Request $request): RedirectResponse
+ {
+ Auth::guard('web')->logout();
+
+ $request->session()->invalidate();
+
+ $request->session()->regenerateToken();
+
+ return redirect('/');
+ }
+}
diff --git a/app/Http/Controllers/Auth/ConfirmablePasswordController.php b/app/Http/Controllers/Auth/ConfirmablePasswordController.php
new file mode 100644
index 0000000..523ddda
--- /dev/null
+++ b/app/Http/Controllers/Auth/ConfirmablePasswordController.php
@@ -0,0 +1,41 @@
+validate([
+ 'email' => $request->user()->email,
+ 'password' => $request->password,
+ ])) {
+ throw ValidationException::withMessages([
+ 'password' => __('auth.password'),
+ ]);
+ }
+
+ $request->session()->put('auth.password_confirmed_at', time());
+
+ return redirect()->intended(RouteServiceProvider::HOME);
+ }
+}
diff --git a/app/Http/Controllers/Auth/EmailVerificationNotificationController.php b/app/Http/Controllers/Auth/EmailVerificationNotificationController.php
new file mode 100644
index 0000000..96ba772
--- /dev/null
+++ b/app/Http/Controllers/Auth/EmailVerificationNotificationController.php
@@ -0,0 +1,25 @@
+user()->hasVerifiedEmail()) {
+ return redirect()->intended(RouteServiceProvider::HOME);
+ }
+
+ $request->user()->sendEmailVerificationNotification();
+
+ return back()->with('status', 'verification-link-sent');
+ }
+}
diff --git a/app/Http/Controllers/Auth/EmailVerificationPromptController.php b/app/Http/Controllers/Auth/EmailVerificationPromptController.php
new file mode 100644
index 0000000..186eb97
--- /dev/null
+++ b/app/Http/Controllers/Auth/EmailVerificationPromptController.php
@@ -0,0 +1,22 @@
+user()->hasVerifiedEmail()
+ ? redirect()->intended(RouteServiceProvider::HOME)
+ : view('auth.verify-email');
+ }
+}
diff --git a/app/Http/Controllers/Auth/NewPasswordController.php b/app/Http/Controllers/Auth/NewPasswordController.php
new file mode 100644
index 0000000..f1e2814
--- /dev/null
+++ b/app/Http/Controllers/Auth/NewPasswordController.php
@@ -0,0 +1,61 @@
+ $request]);
+ }
+
+ /**
+ * Handle an incoming new password request.
+ *
+ * @throws \Illuminate\Validation\ValidationException
+ */
+ public function store(Request $request): RedirectResponse
+ {
+ $request->validate([
+ 'token' => ['required'],
+ 'email' => ['required', 'email'],
+ 'password' => ['required', 'confirmed', Rules\Password::defaults()],
+ ]);
+
+ // Here we will attempt to reset the user's password. If it is successful we
+ // will update the password on an actual user model and persist it to the
+ // database. Otherwise we will parse the error and return the response.
+ $status = Password::reset(
+ $request->only('email', 'password', 'password_confirmation', 'token'),
+ function ($user) use ($request) {
+ $user->forceFill([
+ 'password' => Hash::make($request->password),
+ 'remember_token' => Str::random(60),
+ ])->save();
+
+ event(new PasswordReset($user));
+ }
+ );
+
+ // If the password was successfully reset, we will redirect the user back to
+ // the application's home authenticated view. If there is an error we can
+ // redirect them back to where they came from with their error message.
+ return $status == Password::PASSWORD_RESET
+ ? redirect()->route('login')->with('status', __($status))
+ : back()->withInput($request->only('email'))
+ ->withErrors(['email' => __($status)]);
+ }
+}
diff --git a/app/Http/Controllers/Auth/PasswordController.php b/app/Http/Controllers/Auth/PasswordController.php
new file mode 100644
index 0000000..6916409
--- /dev/null
+++ b/app/Http/Controllers/Auth/PasswordController.php
@@ -0,0 +1,29 @@
+validateWithBag('updatePassword', [
+ 'current_password' => ['required', 'current_password'],
+ 'password' => ['required', Password::defaults(), 'confirmed'],
+ ]);
+
+ $request->user()->update([
+ 'password' => Hash::make($validated['password']),
+ ]);
+
+ return back()->with('status', 'password-updated');
+ }
+}
diff --git a/app/Http/Controllers/Auth/PasswordResetLinkController.php b/app/Http/Controllers/Auth/PasswordResetLinkController.php
new file mode 100644
index 0000000..ce813a6
--- /dev/null
+++ b/app/Http/Controllers/Auth/PasswordResetLinkController.php
@@ -0,0 +1,44 @@
+validate([
+ 'email' => ['required', 'email'],
+ ]);
+
+ // We will send the password reset link to this user. Once we have attempted
+ // to send the link, we will examine the response then see the message we
+ // need to show to the user. Finally, we'll send out a proper response.
+ $status = Password::sendResetLink(
+ $request->only('email')
+ );
+
+ return $status == Password::RESET_LINK_SENT
+ ? back()->with('status', __($status))
+ : back()->withInput($request->only('email'))
+ ->withErrors(['email' => __($status)]);
+ }
+}
diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php
new file mode 100644
index 0000000..5313f35
--- /dev/null
+++ b/app/Http/Controllers/Auth/RegisteredUserController.php
@@ -0,0 +1,51 @@
+validate([
+ 'name' => ['required', 'string', 'max:255'],
+ 'email' => ['required', 'string', 'email', 'max:255', 'unique:'.User::class],
+ 'password' => ['required', 'confirmed', Rules\Password::defaults()],
+ ]);
+
+ $user = User::create([
+ 'name' => $request->name,
+ 'email' => $request->email,
+ 'password' => Hash::make($request->password),
+ ]);
+
+ event(new Registered($user));
+
+ Auth::login($user);
+
+ return redirect(RouteServiceProvider::HOME);
+ }
+}
diff --git a/app/Http/Controllers/Auth/VerifyEmailController.php b/app/Http/Controllers/Auth/VerifyEmailController.php
new file mode 100644
index 0000000..ea87940
--- /dev/null
+++ b/app/Http/Controllers/Auth/VerifyEmailController.php
@@ -0,0 +1,28 @@
+user()->hasVerifiedEmail()) {
+ return redirect()->intended(RouteServiceProvider::HOME.'?verified=1');
+ }
+
+ if ($request->user()->markEmailAsVerified()) {
+ event(new Verified($request->user()));
+ }
+
+ return redirect()->intended(RouteServiceProvider::HOME.'?verified=1');
+ }
+}
diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php
new file mode 100644
index 0000000..a48eb8d
--- /dev/null
+++ b/app/Http/Controllers/ProfileController.php
@@ -0,0 +1,60 @@
+ $request->user(),
+ ]);
+ }
+
+ /**
+ * Update the user's profile information.
+ */
+ public function update(ProfileUpdateRequest $request): RedirectResponse
+ {
+ $request->user()->fill($request->validated());
+
+ if ($request->user()->isDirty('email')) {
+ $request->user()->email_verified_at = null;
+ }
+
+ $request->user()->save();
+
+ return Redirect::route('profile.edit')->with('status', 'profile-updated');
+ }
+
+ /**
+ * Delete the user's account.
+ */
+ public function destroy(Request $request): RedirectResponse
+ {
+ $request->validateWithBag('userDeletion', [
+ 'password' => ['required', 'current_password'],
+ ]);
+
+ $user = $request->user();
+
+ Auth::logout();
+
+ $user->delete();
+
+ $request->session()->invalidate();
+ $request->session()->regenerateToken();
+
+ return Redirect::to('/');
+ }
+}
diff --git a/app/Http/Requests/Auth/LoginRequest.php b/app/Http/Requests/Auth/LoginRequest.php
new file mode 100644
index 0000000..7a19bc0
--- /dev/null
+++ b/app/Http/Requests/Auth/LoginRequest.php
@@ -0,0 +1,85 @@
+
+ */
+ public function rules(): array
+ {
+ return [
+ 'email' => ['required', 'string', 'email'],
+ 'password' => ['required', 'string'],
+ ];
+ }
+
+ /**
+ * Attempt to authenticate the request's credentials.
+ *
+ * @throws \Illuminate\Validation\ValidationException
+ */
+ public function authenticate(): void
+ {
+ $this->ensureIsNotRateLimited();
+
+ if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
+ RateLimiter::hit($this->throttleKey());
+
+ throw ValidationException::withMessages([
+ 'email' => trans('auth.failed'),
+ ]);
+ }
+
+ RateLimiter::clear($this->throttleKey());
+ }
+
+ /**
+ * Ensure the login request is not rate limited.
+ *
+ * @throws \Illuminate\Validation\ValidationException
+ */
+ public function ensureIsNotRateLimited(): void
+ {
+ if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
+ return;
+ }
+
+ event(new Lockout($this));
+
+ $seconds = RateLimiter::availableIn($this->throttleKey());
+
+ throw ValidationException::withMessages([
+ 'email' => trans('auth.throttle', [
+ 'seconds' => $seconds,
+ 'minutes' => ceil($seconds / 60),
+ ]),
+ ]);
+ }
+
+ /**
+ * Get the rate limiting throttle key for the request.
+ */
+ public function throttleKey(): string
+ {
+ return Str::transliterate(Str::lower($this->input('email')).'|'.$this->ip());
+ }
+}
diff --git a/app/Http/Requests/ProfileUpdateRequest.php b/app/Http/Requests/ProfileUpdateRequest.php
new file mode 100644
index 0000000..327ce6f
--- /dev/null
+++ b/app/Http/Requests/ProfileUpdateRequest.php
@@ -0,0 +1,23 @@
+
+ */
+ public function rules(): array
+ {
+ return [
+ 'name' => ['string', 'max:255'],
+ 'email' => ['email', 'max:255', Rule::unique(User::class)->ignore($this->user()->id)],
+ ];
+ }
+}
diff --git a/app/View/Components/AppLayout.php b/app/View/Components/AppLayout.php
new file mode 100644
index 0000000..de0d46f
--- /dev/null
+++ b/app/View/Components/AppLayout.php
@@ -0,0 +1,17 @@
+
+
+ {{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
+
+
+
+
diff --git a/resources/views/auth/forgot-password.blade.php b/resources/views/auth/forgot-password.blade.php
new file mode 100644
index 0000000..3c70788
--- /dev/null
+++ b/resources/views/auth/forgot-password.blade.php
@@ -0,0 +1,25 @@
+
+
+ {{ __('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.') }}
+
+
+
+
+
+
+
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php
new file mode 100644
index 0000000..94d3856
--- /dev/null
+++ b/resources/views/auth/login.blade.php
@@ -0,0 +1,47 @@
+
+
+
+
+
+
diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php
new file mode 100644
index 0000000..759792b
--- /dev/null
+++ b/resources/views/auth/register.blade.php
@@ -0,0 +1,52 @@
+
+
+
diff --git a/resources/views/auth/reset-password.blade.php b/resources/views/auth/reset-password.blade.php
new file mode 100644
index 0000000..a6494cc
--- /dev/null
+++ b/resources/views/auth/reset-password.blade.php
@@ -0,0 +1,39 @@
+
+
+
diff --git a/resources/views/auth/verify-email.blade.php b/resources/views/auth/verify-email.blade.php
new file mode 100644
index 0000000..4e4222f
--- /dev/null
+++ b/resources/views/auth/verify-email.blade.php
@@ -0,0 +1,31 @@
+
+
+ {{ __('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.') }}
+
+
+ @if (session('status') == 'verification-link-sent')
+
+ {{ __('A new verification link has been sent to the email address you provided during registration.') }}
+
+ @endif
+
+
+
diff --git a/resources/views/components/application-logo.blade.php b/resources/views/components/application-logo.blade.php
new file mode 100644
index 0000000..46579cf
--- /dev/null
+++ b/resources/views/components/application-logo.blade.php
@@ -0,0 +1,3 @@
+
+
+
diff --git a/resources/views/components/auth-session-status.blade.php b/resources/views/components/auth-session-status.blade.php
new file mode 100644
index 0000000..a39bc7d
--- /dev/null
+++ b/resources/views/components/auth-session-status.blade.php
@@ -0,0 +1,7 @@
+@props(['status'])
+
+@if ($status)
+ merge(['class' => 'font-medium text-sm text-green-600 dark:text-green-400']) }}>
+ {{ $status }}
+
+@endif
diff --git a/resources/views/components/danger-button.blade.php b/resources/views/components/danger-button.blade.php
new file mode 100644
index 0000000..d7417b2
--- /dev/null
+++ b/resources/views/components/danger-button.blade.php
@@ -0,0 +1,3 @@
+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']) }}>
+ {{ $slot }}
+
diff --git a/resources/views/components/dropdown-link.blade.php b/resources/views/components/dropdown-link.blade.php
new file mode 100644
index 0000000..6b54bfb
--- /dev/null
+++ b/resources/views/components/dropdown-link.blade.php
@@ -0,0 +1 @@
+merge(['class' => 'block w-full px-4 py-2 text-left 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 }}
diff --git a/resources/views/components/dropdown.blade.php b/resources/views/components/dropdown.blade.php
new file mode 100644
index 0000000..f51d335
--- /dev/null
+++ b/resources/views/components/dropdown.blade.php
@@ -0,0 +1,43 @@
+@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white dark:bg-gray-700'])
+
+@php
+switch ($align) {
+ case 'left':
+ $alignmentClasses = 'origin-top-left left-0';
+ break;
+ case 'top':
+ $alignmentClasses = 'origin-top';
+ break;
+ case 'right':
+ default:
+ $alignmentClasses = 'origin-top-right right-0';
+ break;
+}
+
+switch ($width) {
+ case '48':
+ $width = 'w-48';
+ break;
+}
+@endphp
+
+
+
+ {{ $trigger }}
+
+
+
+
diff --git a/resources/views/components/input-error.blade.php b/resources/views/components/input-error.blade.php
new file mode 100644
index 0000000..ad95f6b
--- /dev/null
+++ b/resources/views/components/input-error.blade.php
@@ -0,0 +1,9 @@
+@props(['messages'])
+
+@if ($messages)
+ merge(['class' => 'text-sm text-red-600 dark:text-red-400 space-y-1']) }}>
+ @foreach ((array) $messages as $message)
+ {{ $message }}
+ @endforeach
+
+@endif
diff --git a/resources/views/components/input-label.blade.php b/resources/views/components/input-label.blade.php
new file mode 100644
index 0000000..e93b059
--- /dev/null
+++ b/resources/views/components/input-label.blade.php
@@ -0,0 +1,5 @@
+@props(['value'])
+
+merge(['class' => 'block font-medium text-sm text-gray-700 dark:text-gray-300']) }}>
+ {{ $value ?? $slot }}
+
diff --git a/resources/views/components/modal.blade.php b/resources/views/components/modal.blade.php
new file mode 100644
index 0000000..4271d97
--- /dev/null
+++ b/resources/views/components/modal.blade.php
@@ -0,0 +1,77 @@
+@props([
+ 'name',
+ 'show' => false,
+ 'maxWidth' => '2xl'
+])
+
+@php
+$maxWidth = [
+ 'sm' => 'sm:max-w-sm',
+ 'md' => 'sm:max-w-md',
+ 'lg' => 'sm:max-w-lg',
+ 'xl' => 'sm:max-w-xl',
+ '2xl' => 'sm:max-w-2xl',
+][$maxWidth];
+@endphp
+
+
diff --git a/resources/views/components/nav-link.blade.php b/resources/views/components/nav-link.blade.php
new file mode 100644
index 0000000..37bad55
--- /dev/null
+++ b/resources/views/components/nav-link.blade.php
@@ -0,0 +1,11 @@
+@props(['active'])
+
+@php
+$classes = ($active ?? false)
+ ? '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'
+ : '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';
+@endphp
+
+merge(['class' => $classes]) }}>
+ {{ $slot }}
+
diff --git a/resources/views/components/primary-button.blade.php b/resources/views/components/primary-button.blade.php
new file mode 100644
index 0000000..99bf389
--- /dev/null
+++ b/resources/views/components/primary-button.blade.php
@@ -0,0 +1,3 @@
+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']) }}>
+ {{ $slot }}
+
diff --git a/resources/views/components/responsive-nav-link.blade.php b/resources/views/components/responsive-nav-link.blade.php
new file mode 100644
index 0000000..1148d9a
--- /dev/null
+++ b/resources/views/components/responsive-nav-link.blade.php
@@ -0,0 +1,11 @@
+@props(['active'])
+
+@php
+$classes = ($active ?? false)
+ ? 'block w-full pl-3 pr-4 py-2 border-l-4 border-indigo-400 dark:border-indigo-600 text-left 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'
+ : 'block w-full pl-3 pr-4 py-2 border-l-4 border-transparent text-left 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';
+@endphp
+
+merge(['class' => $classes]) }}>
+ {{ $slot }}
+
diff --git a/resources/views/components/secondary-button.blade.php b/resources/views/components/secondary-button.blade.php
new file mode 100644
index 0000000..fa1c549
--- /dev/null
+++ b/resources/views/components/secondary-button.blade.php
@@ -0,0 +1,3 @@
+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']) }}>
+ {{ $slot }}
+
diff --git a/resources/views/components/text-input.blade.php b/resources/views/components/text-input.blade.php
new file mode 100644
index 0000000..7779a13
--- /dev/null
+++ b/resources/views/components/text-input.blade.php
@@ -0,0 +1,3 @@
+@props(['disabled' => false])
+
+ 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']) !!}>
diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php
new file mode 100644
index 0000000..4024c64
--- /dev/null
+++ b/resources/views/dashboard.blade.php
@@ -0,0 +1,17 @@
+
+
+
+ {{ __('Dashboard') }}
+
+
+
+
+
+
+
+ {{ __("You're logged in!") }}
+
+
+
+
+
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php
new file mode 100644
index 0000000..9069c10
--- /dev/null
+++ b/resources/views/layouts/app.blade.php
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+ {{ config('app.name', 'Laravel') }}
+
+
+
+
+
+
+ @vite(['resources/css/app.css', 'resources/js/app.js'])
+
+
+
+ @include('layouts.navigation')
+
+
+ @if (isset($header))
+
+ @endif
+
+
+
+ {{ $slot }}
+
+
+
+
diff --git a/resources/views/layouts/guest.blade.php b/resources/views/layouts/guest.blade.php
new file mode 100644
index 0000000..4b369b6
--- /dev/null
+++ b/resources/views/layouts/guest.blade.php
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ {{ config('app.name', 'Laravel') }}
+
+
+
+
+
+
+ @vite(['resources/css/app.css', 'resources/js/app.js'])
+
+
+
+
+
diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php
new file mode 100644
index 0000000..6b145a8
--- /dev/null
+++ b/resources/views/layouts/navigation.blade.php
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ __('Dashboard') }}
+
+
+
+
+
+
+
+
+
+ {{ Auth::user()->name }}
+
+
+
+
+
+
+
+ {{ __('Profile') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ __('Dashboard') }}
+
+
+
+
+
+
+
{{ Auth::user()->name }}
+
{{ Auth::user()->email }}
+
+
+
+
+ {{ __('Profile') }}
+
+
+
+
+
+
+
+
diff --git a/resources/views/profile/edit.blade.php b/resources/views/profile/edit.blade.php
new file mode 100644
index 0000000..ef69910
--- /dev/null
+++ b/resources/views/profile/edit.blade.php
@@ -0,0 +1,29 @@
+
+
+
+ {{ __('Profile') }}
+
+
+
+
+
+
+
+ @include('profile.partials.update-profile-information-form')
+
+
+
+
+
+ @include('profile.partials.update-password-form')
+
+
+
+
+
+ @include('profile.partials.delete-user-form')
+
+
+
+
+
diff --git a/resources/views/profile/partials/delete-user-form.blade.php b/resources/views/profile/partials/delete-user-form.blade.php
new file mode 100644
index 0000000..87d31b5
--- /dev/null
+++ b/resources/views/profile/partials/delete-user-form.blade.php
@@ -0,0 +1,55 @@
+
+
+
+ {{ __('Delete Account') }}
+
+
+
+ {{ __('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.') }}
+
+
+
+ {{ __('Delete Account') }}
+
+
+
+
+
diff --git a/resources/views/profile/partials/update-password-form.blade.php b/resources/views/profile/partials/update-password-form.blade.php
new file mode 100644
index 0000000..a66e1ac
--- /dev/null
+++ b/resources/views/profile/partials/update-password-form.blade.php
@@ -0,0 +1,48 @@
+
diff --git a/resources/views/profile/partials/update-profile-information-form.blade.php b/resources/views/profile/partials/update-profile-information-form.blade.php
new file mode 100644
index 0000000..7273fff
--- /dev/null
+++ b/resources/views/profile/partials/update-profile-information-form.blade.php
@@ -0,0 +1,64 @@
+
diff --git a/routes/auth.php b/routes/auth.php
new file mode 100644
index 0000000..1040b51
--- /dev/null
+++ b/routes/auth.php
@@ -0,0 +1,59 @@
+group(function () {
+ Route::get('register', [RegisteredUserController::class, 'create'])
+ ->name('register');
+
+ Route::post('register', [RegisteredUserController::class, 'store']);
+
+ Route::get('login', [AuthenticatedSessionController::class, 'create'])
+ ->name('login');
+
+ Route::post('login', [AuthenticatedSessionController::class, 'store']);
+
+ Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
+ ->name('password.request');
+
+ Route::post('forgot-password', [PasswordResetLinkController::class, 'store'])
+ ->name('password.email');
+
+ Route::get('reset-password/{token}', [NewPasswordController::class, 'create'])
+ ->name('password.reset');
+
+ Route::post('reset-password', [NewPasswordController::class, 'store'])
+ ->name('password.store');
+});
+
+Route::middleware('auth')->group(function () {
+ Route::get('verify-email', EmailVerificationPromptController::class)
+ ->name('verification.notice');
+
+ Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
+ ->middleware(['signed', 'throttle:6,1'])
+ ->name('verification.verify');
+
+ Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store'])
+ ->middleware('throttle:6,1')
+ ->name('verification.send');
+
+ Route::get('confirm-password', [ConfirmablePasswordController::class, 'show'])
+ ->name('password.confirm');
+
+ Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']);
+
+ Route::put('password', [PasswordController::class, 'update'])->name('password.update');
+
+ Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
+ ->name('logout');
+});
diff --git a/tests/Feature/Auth/AuthenticationTest.php b/tests/Feature/Auth/AuthenticationTest.php
new file mode 100644
index 0000000..2d0eeed
--- /dev/null
+++ b/tests/Feature/Auth/AuthenticationTest.php
@@ -0,0 +1,45 @@
+get('/login');
+
+ $response->assertStatus(200);
+ }
+
+ public function test_users_can_authenticate_using_the_login_screen(): void
+ {
+ $user = User::factory()->create();
+
+ $response = $this->post('/login', [
+ 'email' => $user->email,
+ 'password' => 'password',
+ ]);
+
+ $this->assertAuthenticated();
+ $response->assertRedirect(RouteServiceProvider::HOME);
+ }
+
+ public function test_users_can_not_authenticate_with_invalid_password(): void
+ {
+ $user = User::factory()->create();
+
+ $this->post('/login', [
+ 'email' => $user->email,
+ 'password' => 'wrong-password',
+ ]);
+
+ $this->assertGuest();
+ }
+}
diff --git a/tests/Feature/Auth/EmailVerificationTest.php b/tests/Feature/Auth/EmailVerificationTest.php
new file mode 100644
index 0000000..ba19d9c
--- /dev/null
+++ b/tests/Feature/Auth/EmailVerificationTest.php
@@ -0,0 +1,65 @@
+create([
+ 'email_verified_at' => null,
+ ]);
+
+ $response = $this->actingAs($user)->get('/verify-email');
+
+ $response->assertStatus(200);
+ }
+
+ public function test_email_can_be_verified(): void
+ {
+ $user = User::factory()->create([
+ 'email_verified_at' => null,
+ ]);
+
+ Event::fake();
+
+ $verificationUrl = URL::temporarySignedRoute(
+ 'verification.verify',
+ now()->addMinutes(60),
+ ['id' => $user->id, 'hash' => sha1($user->email)]
+ );
+
+ $response = $this->actingAs($user)->get($verificationUrl);
+
+ Event::assertDispatched(Verified::class);
+ $this->assertTrue($user->fresh()->hasVerifiedEmail());
+ $response->assertRedirect(RouteServiceProvider::HOME.'?verified=1');
+ }
+
+ public function test_email_is_not_verified_with_invalid_hash(): void
+ {
+ $user = User::factory()->create([
+ 'email_verified_at' => null,
+ ]);
+
+ $verificationUrl = URL::temporarySignedRoute(
+ 'verification.verify',
+ now()->addMinutes(60),
+ ['id' => $user->id, 'hash' => sha1('wrong-email')]
+ );
+
+ $this->actingAs($user)->get($verificationUrl);
+
+ $this->assertFalse($user->fresh()->hasVerifiedEmail());
+ }
+}
diff --git a/tests/Feature/Auth/PasswordConfirmationTest.php b/tests/Feature/Auth/PasswordConfirmationTest.php
new file mode 100644
index 0000000..ff85721
--- /dev/null
+++ b/tests/Feature/Auth/PasswordConfirmationTest.php
@@ -0,0 +1,44 @@
+create();
+
+ $response = $this->actingAs($user)->get('/confirm-password');
+
+ $response->assertStatus(200);
+ }
+
+ public function test_password_can_be_confirmed(): void
+ {
+ $user = User::factory()->create();
+
+ $response = $this->actingAs($user)->post('/confirm-password', [
+ 'password' => 'password',
+ ]);
+
+ $response->assertRedirect();
+ $response->assertSessionHasNoErrors();
+ }
+
+ public function test_password_is_not_confirmed_with_invalid_password(): void
+ {
+ $user = User::factory()->create();
+
+ $response = $this->actingAs($user)->post('/confirm-password', [
+ 'password' => 'wrong-password',
+ ]);
+
+ $response->assertSessionHasErrors();
+ }
+}
diff --git a/tests/Feature/Auth/PasswordResetTest.php b/tests/Feature/Auth/PasswordResetTest.php
new file mode 100644
index 0000000..4a26065
--- /dev/null
+++ b/tests/Feature/Auth/PasswordResetTest.php
@@ -0,0 +1,71 @@
+get('/forgot-password');
+
+ $response->assertStatus(200);
+ }
+
+ public function test_reset_password_link_can_be_requested(): void
+ {
+ Notification::fake();
+
+ $user = User::factory()->create();
+
+ $this->post('/forgot-password', ['email' => $user->email]);
+
+ Notification::assertSentTo($user, ResetPassword::class);
+ }
+
+ public function test_reset_password_screen_can_be_rendered(): void
+ {
+ Notification::fake();
+
+ $user = User::factory()->create();
+
+ $this->post('/forgot-password', ['email' => $user->email]);
+
+ Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
+ $response = $this->get('/reset-password/'.$notification->token);
+
+ $response->assertStatus(200);
+
+ return true;
+ });
+ }
+
+ public function test_password_can_be_reset_with_valid_token(): void
+ {
+ Notification::fake();
+
+ $user = User::factory()->create();
+
+ $this->post('/forgot-password', ['email' => $user->email]);
+
+ Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
+ $response = $this->post('/reset-password', [
+ 'token' => $notification->token,
+ 'email' => $user->email,
+ 'password' => 'password',
+ 'password_confirmation' => 'password',
+ ]);
+
+ $response->assertSessionHasNoErrors();
+
+ return true;
+ });
+ }
+}
diff --git a/tests/Feature/Auth/PasswordUpdateTest.php b/tests/Feature/Auth/PasswordUpdateTest.php
new file mode 100644
index 0000000..ca28c6c
--- /dev/null
+++ b/tests/Feature/Auth/PasswordUpdateTest.php
@@ -0,0 +1,51 @@
+create();
+
+ $response = $this
+ ->actingAs($user)
+ ->from('/profile')
+ ->put('/password', [
+ 'current_password' => 'password',
+ 'password' => 'new-password',
+ 'password_confirmation' => 'new-password',
+ ]);
+
+ $response
+ ->assertSessionHasNoErrors()
+ ->assertRedirect('/profile');
+
+ $this->assertTrue(Hash::check('new-password', $user->refresh()->password));
+ }
+
+ public function test_correct_password_must_be_provided_to_update_password(): void
+ {
+ $user = User::factory()->create();
+
+ $response = $this
+ ->actingAs($user)
+ ->from('/profile')
+ ->put('/password', [
+ 'current_password' => 'wrong-password',
+ 'password' => 'new-password',
+ 'password_confirmation' => 'new-password',
+ ]);
+
+ $response
+ ->assertSessionHasErrorsIn('updatePassword', 'current_password')
+ ->assertRedirect('/profile');
+ }
+}
diff --git a/tests/Feature/Auth/RegistrationTest.php b/tests/Feature/Auth/RegistrationTest.php
new file mode 100644
index 0000000..30829b1
--- /dev/null
+++ b/tests/Feature/Auth/RegistrationTest.php
@@ -0,0 +1,32 @@
+get('/register');
+
+ $response->assertStatus(200);
+ }
+
+ public function test_new_users_can_register(): void
+ {
+ $response = $this->post('/register', [
+ 'name' => 'Test User',
+ 'email' => 'test@example.com',
+ 'password' => 'password',
+ 'password_confirmation' => 'password',
+ ]);
+
+ $this->assertAuthenticated();
+ $response->assertRedirect(RouteServiceProvider::HOME);
+ }
+}
diff --git a/tests/Feature/ProfileTest.php b/tests/Feature/ProfileTest.php
new file mode 100644
index 0000000..252fdcc
--- /dev/null
+++ b/tests/Feature/ProfileTest.php
@@ -0,0 +1,99 @@
+create();
+
+ $response = $this
+ ->actingAs($user)
+ ->get('/profile');
+
+ $response->assertOk();
+ }
+
+ public function test_profile_information_can_be_updated(): void
+ {
+ $user = User::factory()->create();
+
+ $response = $this
+ ->actingAs($user)
+ ->patch('/profile', [
+ 'name' => 'Test User',
+ 'email' => 'test@example.com',
+ ]);
+
+ $response
+ ->assertSessionHasNoErrors()
+ ->assertRedirect('/profile');
+
+ $user->refresh();
+
+ $this->assertSame('Test User', $user->name);
+ $this->assertSame('test@example.com', $user->email);
+ $this->assertNull($user->email_verified_at);
+ }
+
+ public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged(): void
+ {
+ $user = User::factory()->create();
+
+ $response = $this
+ ->actingAs($user)
+ ->patch('/profile', [
+ 'name' => 'Test User',
+ 'email' => $user->email,
+ ]);
+
+ $response
+ ->assertSessionHasNoErrors()
+ ->assertRedirect('/profile');
+
+ $this->assertNotNull($user->refresh()->email_verified_at);
+ }
+
+ public function test_user_can_delete_their_account(): void
+ {
+ $user = User::factory()->create();
+
+ $response = $this
+ ->actingAs($user)
+ ->delete('/profile', [
+ 'password' => 'password',
+ ]);
+
+ $response
+ ->assertSessionHasNoErrors()
+ ->assertRedirect('/');
+
+ $this->assertGuest();
+ $this->assertNull($user->fresh());
+ }
+
+ public function test_correct_password_must_be_provided_to_delete_account(): void
+ {
+ $user = User::factory()->create();
+
+ $response = $this
+ ->actingAs($user)
+ ->from('/profile')
+ ->delete('/profile', [
+ 'password' => 'wrong-password',
+ ]);
+
+ $response
+ ->assertSessionHasErrorsIn('userDeletion', 'password')
+ ->assertRedirect('/profile');
+
+ $this->assertNotNull($user->fresh());
+ }
+}