-
Let's get started
+
+ Let's get started
+
Laravel has an incredibly rich ecosystem.
@@ -130,7 +136,10 @@ export default function Welcome() {
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
-
+
-
-
+
+
- createInertiaApp({
- page,
- render: ReactDOMServer.renderToString,
- resolve: (name) => {
- const pages = import.meta.glob('./pages/**/*.tsx', {
- eager: true,
- });
- return pages[`./pages/${name}.tsx`];
- },
- // prettier-ignore
- setup: ({ App, props }) => ,
- }),
-);
diff --git a/resources/js/ssr.tsx b/resources/js/ssr.tsx
new file mode 100644
index 000000000..a868a5c68
--- /dev/null
+++ b/resources/js/ssr.tsx
@@ -0,0 +1,22 @@
+import { createInertiaApp } from '@inertiajs/react';
+import createServer from '@inertiajs/react/server';
+import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
+import ReactDOMServer from 'react-dom/server';
+
+const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
+
+createServer((page) =>
+ createInertiaApp({
+ page,
+ render: ReactDOMServer.renderToString,
+ title: (title) => (title ? `${title} - ${appName}` : appName),
+ resolve: (name) =>
+ resolvePageComponent(
+ `./pages/${name}.tsx`,
+ import.meta.glob('./pages/**/*.tsx'),
+ ),
+ setup: ({ App, props }) => {
+ return ;
+ },
+ }),
+);
diff --git a/resources/js/types/index.ts b/resources/js/types/index.d.ts
similarity index 81%
rename from resources/js/types/index.ts
rename to resources/js/types/index.d.ts
index 9929c24d1..2f10844c7 100644
--- a/resources/js/types/index.ts
+++ b/resources/js/types/index.d.ts
@@ -1,3 +1,4 @@
+import { InertiaLinkProps } from '@inertiajs/react';
import { LucideIcon } from 'lucide-react';
export interface Auth {
@@ -16,7 +17,7 @@ export interface NavGroup {
export interface NavItem {
title: string;
- url: string;
+ href: NonNullable;
icon?: LucideIcon | null;
isActive?: boolean;
}
@@ -25,6 +26,7 @@ export interface SharedData {
name: string;
quote: { message: string; author: string };
auth: Auth;
+ sidebarOpen: boolean;
[key: string]: unknown;
}
@@ -34,6 +36,7 @@ export interface User {
email: string;
avatar?: string;
email_verified_at: string | null;
+ two_factor_enabled?: boolean;
created_at: string;
updated_at: string;
[key: string]: unknown; // This allows for additional properties...
diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php
index 8560cbc0d..c84bff73c 100644
--- a/resources/views/app.blade.php
+++ b/resources/views/app.blade.php
@@ -1,15 +1,44 @@
-
+ ($appearance ?? 'system') == 'dark'])>
+ {{-- Inline script to detect system dark mode preference and apply it immediately --}}
+
+
+ {{-- Inline style to set the HTML background color based on our theme in app.css --}}
+
+
{{ config('app.name', 'Laravel') }}
+
+
+
+
- @routes
@viteReactRefresh
@vite(['resources/js/app.tsx', "resources/js/pages/{$page['component']}.tsx"])
@inertiaHead
diff --git a/routes/auth.php b/routes/auth.php
index 7862ed46c..191847eba 100644
--- a/routes/auth.php
+++ b/routes/auth.php
@@ -1,7 +1,6 @@
name('register');
- Route::post('register', [RegisteredUserController::class, 'store']);
+ Route::post('register', [RegisteredUserController::class, 'store'])
+ ->name('register.store');
Route::get('login', [AuthenticatedSessionController::class, 'create'])
->name('login');
- Route::post('login', [AuthenticatedSessionController::class, 'store']);
+ Route::post('login', [AuthenticatedSessionController::class, 'store'])
+ ->name('login.store');
Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
->name('password.request');
@@ -46,11 +47,6 @@
->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::post('logout', [AuthenticatedSessionController::class, 'destroy'])
->name('logout');
});
diff --git a/routes/settings.php b/routes/settings.php
index 95031371c..98dd9d730 100644
--- a/routes/settings.php
+++ b/routes/settings.php
@@ -2,20 +2,27 @@
use App\Http\Controllers\Settings\PasswordController;
use App\Http\Controllers\Settings\ProfileController;
+use App\Http\Controllers\Settings\TwoFactorAuthenticationController;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::middleware('auth')->group(function () {
- Route::redirect('settings', 'settings/profile');
+ Route::redirect('settings', '/settings/profile');
Route::get('settings/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('settings/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('settings/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
Route::get('settings/password', [PasswordController::class, 'edit'])->name('password.edit');
- Route::put('settings/password', [PasswordController::class, 'update'])->name('password.update');
+
+ Route::put('settings/password', [PasswordController::class, 'update'])
+ ->middleware('throttle:6,1')
+ ->name('password.update');
Route::get('settings/appearance', function () {
return Inertia::render('settings/appearance');
- })->name('appearance');
+ })->name('appearance.edit');
+
+ Route::get('settings/two-factor', [TwoFactorAuthenticationController::class, 'show'])
+ ->name('two-factor.show');
});
diff --git a/tests/Feature/Auth/AuthenticationTest.php b/tests/Feature/Auth/AuthenticationTest.php
index c59d166a8..7c2a12367 100644
--- a/tests/Feature/Auth/AuthenticationTest.php
+++ b/tests/Feature/Auth/AuthenticationTest.php
@@ -4,6 +4,8 @@
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\RateLimiter;
+use Laravel\Fortify\Features;
use Tests\TestCase;
class AuthenticationTest extends TestCase
@@ -12,7 +14,7 @@ class AuthenticationTest extends TestCase
public function test_login_screen_can_be_rendered()
{
- $response = $this->get('/login');
+ $response = $this->get(route('login'));
$response->assertStatus(200);
}
@@ -21,7 +23,7 @@ public function test_users_can_authenticate_using_the_login_screen()
{
$user = User::factory()->create();
- $response = $this->post('/login', [
+ $response = $this->post(route('login.store'), [
'email' => $user->email,
'password' => 'password',
]);
@@ -30,11 +32,40 @@ public function test_users_can_authenticate_using_the_login_screen()
$response->assertRedirect(route('dashboard', absolute: false));
}
+ public function test_users_with_two_factor_enabled_are_redirected_to_two_factor_challenge()
+ {
+ if (! Features::canManageTwoFactorAuthentication()) {
+ $this->markTestSkipped('Two-factor authentication is not enabled.');
+ }
+
+ Features::twoFactorAuthentication([
+ 'confirm' => true,
+ 'confirmPassword' => true,
+ ]);
+
+ $user = User::factory()->create();
+
+ $user->forceFill([
+ 'two_factor_secret' => encrypt('test-secret'),
+ 'two_factor_recovery_codes' => encrypt(json_encode(['code1', 'code2'])),
+ 'two_factor_confirmed_at' => now(),
+ ])->save();
+
+ $response = $this->post(route('login'), [
+ 'email' => $user->email,
+ 'password' => 'password',
+ ]);
+
+ $response->assertRedirect(route('two-factor.login'));
+ $response->assertSessionHas('login.id', $user->id);
+ $this->assertGuest();
+ }
+
public function test_users_can_not_authenticate_with_invalid_password()
{
$user = User::factory()->create();
- $this->post('/login', [
+ $this->post(route('login.store'), [
'email' => $user->email,
'password' => 'wrong-password',
]);
@@ -46,9 +77,27 @@ public function test_users_can_logout()
{
$user = User::factory()->create();
- $response = $this->actingAs($user)->post('/logout');
+ $response = $this->actingAs($user)->post(route('logout'));
$this->assertGuest();
- $response->assertRedirect('/');
+ $response->assertRedirect(route('home'));
+ }
+
+ public function test_users_are_rate_limited()
+ {
+ $user = User::factory()->create();
+
+ RateLimiter::increment(implode('|', [$user->email, '127.0.0.1']), amount: 10);
+
+ $response = $this->post(route('login.store'), [
+ 'email' => $user->email,
+ 'password' => 'wrong-password',
+ ]);
+
+ $response->assertSessionHasErrors('email');
+
+ $errors = session('errors');
+
+ $this->assertStringContainsString('Too many login attempts', $errors->first('email'));
}
}
diff --git a/tests/Feature/Auth/EmailVerificationTest.php b/tests/Feature/Auth/EmailVerificationTest.php
index 627fe709d..8eaba3cfe 100644
--- a/tests/Feature/Auth/EmailVerificationTest.php
+++ b/tests/Feature/Auth/EmailVerificationTest.php
@@ -17,7 +17,7 @@ public function test_email_verification_screen_can_be_rendered()
{
$user = User::factory()->unverified()->create();
- $response = $this->actingAs($user)->get('/verify-email');
+ $response = $this->actingAs($user)->get(route('verification.notice'));
$response->assertStatus(200);
}
@@ -55,4 +55,53 @@ public function test_email_is_not_verified_with_invalid_hash()
$this->assertFalse($user->fresh()->hasVerifiedEmail());
}
+
+ public function test_email_is_not_verified_with_invalid_user_id(): void
+ {
+ $user = User::factory()->create([
+ 'email_verified_at' => null,
+ ]);
+
+ $verificationUrl = URL::temporarySignedRoute(
+ 'verification.verify',
+ now()->addMinutes(60),
+ ['id' => 123, 'hash' => sha1($user->email)]
+ );
+
+ $this->actingAs($user)->get($verificationUrl);
+
+ $this->assertFalse($user->fresh()->hasVerifiedEmail());
+ }
+
+ public function test_verified_user_is_redirected_to_dashboard_from_verification_prompt(): void
+ {
+ $user = User::factory()->create([
+ 'email_verified_at' => now(),
+ ]);
+
+ $response = $this->actingAs($user)->get(route('verification.notice'));
+
+ $response->assertRedirect(route('dashboard', absolute: false));
+ }
+
+ public function test_already_verified_user_visiting_verification_link_is_redirected_without_firing_event_again(): void
+ {
+ $user = User::factory()->create([
+ 'email_verified_at' => now(),
+ ]);
+
+ Event::fake();
+
+ $verificationUrl = URL::temporarySignedRoute(
+ 'verification.verify',
+ now()->addMinutes(60),
+ ['id' => $user->id, 'hash' => sha1($user->email)]
+ );
+
+ $this->actingAs($user)->get($verificationUrl)
+ ->assertRedirect(route('dashboard', absolute: false).'?verified=1');
+
+ $this->assertTrue($user->fresh()->hasVerifiedEmail());
+ Event::assertNotDispatched(Verified::class);
+ }
}
diff --git a/tests/Feature/Auth/PasswordConfirmationTest.php b/tests/Feature/Auth/PasswordConfirmationTest.php
index d2072ffd4..5191f9c20 100644
--- a/tests/Feature/Auth/PasswordConfirmationTest.php
+++ b/tests/Feature/Auth/PasswordConfirmationTest.php
@@ -4,6 +4,7 @@
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
+use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
class PasswordConfirmationTest extends TestCase
@@ -14,31 +15,19 @@ public function test_confirm_password_screen_can_be_rendered()
{
$user = User::factory()->create();
- $response = $this->actingAs($user)->get('/confirm-password');
+ $response = $this->actingAs($user)->get(route('password.confirm'));
$response->assertStatus(200);
- }
-
- public function test_password_can_be_confirmed()
- {
- $user = User::factory()->create();
- $response = $this->actingAs($user)->post('/confirm-password', [
- 'password' => 'password',
- ]);
-
- $response->assertRedirect();
- $response->assertSessionHasNoErrors();
+ $response->assertInertia(fn (Assert $page) => $page
+ ->component('auth/confirm-password')
+ );
}
- public function test_password_is_not_confirmed_with_invalid_password()
+ public function test_password_confirmation_requires_authentication()
{
- $user = User::factory()->create();
-
- $response = $this->actingAs($user)->post('/confirm-password', [
- 'password' => 'wrong-password',
- ]);
+ $response = $this->get(route('password.confirm'));
- $response->assertSessionHasErrors();
+ $response->assertRedirect(route('login'));
}
}
diff --git a/tests/Feature/Auth/PasswordResetTest.php b/tests/Feature/Auth/PasswordResetTest.php
index 3c7441f78..6737c2a76 100644
--- a/tests/Feature/Auth/PasswordResetTest.php
+++ b/tests/Feature/Auth/PasswordResetTest.php
@@ -14,7 +14,7 @@ class PasswordResetTest extends TestCase
public function test_reset_password_link_screen_can_be_rendered()
{
- $response = $this->get('/forgot-password');
+ $response = $this->get(route('password.request'));
$response->assertStatus(200);
}
@@ -25,7 +25,7 @@ public function test_reset_password_link_can_be_requested()
$user = User::factory()->create();
- $this->post('/forgot-password', ['email' => $user->email]);
+ $this->post(route('password.email'), ['email' => $user->email]);
Notification::assertSentTo($user, ResetPassword::class);
}
@@ -36,10 +36,10 @@ public function test_reset_password_screen_can_be_rendered()
$user = User::factory()->create();
- $this->post('/forgot-password', ['email' => $user->email]);
+ $this->post(route('password.email'), ['email' => $user->email]);
Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
- $response = $this->get('/reset-password/'.$notification->token);
+ $response = $this->get(route('password.reset', $notification->token));
$response->assertStatus(200);
@@ -53,10 +53,10 @@ public function test_password_can_be_reset_with_valid_token()
$user = User::factory()->create();
- $this->post('/forgot-password', ['email' => $user->email]);
+ $this->post(route('password.email'), ['email' => $user->email]);
Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
- $response = $this->post('/reset-password', [
+ $response = $this->post(route('password.store'), [
'token' => $notification->token,
'email' => $user->email,
'password' => 'password',
@@ -70,4 +70,18 @@ public function test_password_can_be_reset_with_valid_token()
return true;
});
}
+
+ public function test_password_cannot_be_reset_with_invalid_token(): void
+ {
+ $user = User::factory()->create();
+
+ $response = $this->post(route('password.store'), [
+ 'token' => 'invalid-token',
+ 'email' => $user->email,
+ 'password' => 'newpassword123',
+ 'password_confirmation' => 'newpassword123',
+ ]);
+
+ $response->assertSessionHasErrors('email');
+ }
}
diff --git a/tests/Feature/Auth/RegistrationTest.php b/tests/Feature/Auth/RegistrationTest.php
index d0c3ea257..16cc907dc 100644
--- a/tests/Feature/Auth/RegistrationTest.php
+++ b/tests/Feature/Auth/RegistrationTest.php
@@ -11,14 +11,14 @@ class RegistrationTest extends TestCase
public function test_registration_screen_can_be_rendered()
{
- $response = $this->get('/register');
+ $response = $this->get(route('register'));
$response->assertStatus(200);
}
public function test_new_users_can_register()
{
- $response = $this->post('/register', [
+ $response = $this->post(route('register.store'), [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password',
diff --git a/tests/Feature/Auth/TwoFactorChallengeTest.php b/tests/Feature/Auth/TwoFactorChallengeTest.php
new file mode 100644
index 000000000..b8d6b803e
--- /dev/null
+++ b/tests/Feature/Auth/TwoFactorChallengeTest.php
@@ -0,0 +1,56 @@
+markTestSkipped('Two-factor authentication is not enabled.');
+ }
+
+ $response = $this->get(route('two-factor.login'));
+
+ $response->assertRedirect(route('login'));
+ }
+
+ public function test_two_factor_challenge_can_be_rendered(): void
+ {
+ if (! Features::canManageTwoFactorAuthentication()) {
+ $this->markTestSkipped('Two-factor authentication is not enabled.');
+ }
+
+ Features::twoFactorAuthentication([
+ 'confirm' => true,
+ 'confirmPassword' => true,
+ ]);
+
+ $user = User::factory()->create();
+
+ $user->forceFill([
+ 'two_factor_secret' => encrypt('test-secret'),
+ 'two_factor_recovery_codes' => encrypt(json_encode(['code1', 'code2'])),
+ 'two_factor_confirmed_at' => now(),
+ ])->save();
+
+ $this->post(route('login'), [
+ 'email' => $user->email,
+ 'password' => 'password',
+ ]);
+
+ $this->get(route('two-factor.login'))
+ ->assertOk()
+ ->assertInertia(fn (Assert $page) => $page
+ ->component('auth/two-factor-challenge')
+ );
+ }
+}
diff --git a/tests/Feature/Auth/VerificationNotificationTest.php b/tests/Feature/Auth/VerificationNotificationTest.php
new file mode 100644
index 000000000..34a81a81a
--- /dev/null
+++ b/tests/Feature/Auth/VerificationNotificationTest.php
@@ -0,0 +1,44 @@
+create([
+ 'email_verified_at' => null,
+ ]);
+
+ $this->actingAs($user)
+ ->post(route('verification.send'))
+ ->assertRedirect(route('home'));
+
+ Notification::assertSentTo($user, VerifyEmail::class);
+ }
+
+ public function test_does_not_send_verification_notification_if_email_is_verified(): void
+ {
+ Notification::fake();
+
+ $user = User::factory()->create([
+ 'email_verified_at' => now(),
+ ]);
+
+ $this->actingAs($user)
+ ->post(route('verification.send'))
+ ->assertRedirect(route('dashboard', absolute: false));
+
+ Notification::assertNothingSent();
+ }
+}
diff --git a/tests/Feature/DashboardTest.php b/tests/Feature/DashboardTest.php
index 8585ade69..2cf1dc29d 100644
--- a/tests/Feature/DashboardTest.php
+++ b/tests/Feature/DashboardTest.php
@@ -12,13 +12,13 @@ class DashboardTest extends TestCase
public function test_guests_are_redirected_to_the_login_page()
{
- $this->get('/dashboard')->assertRedirect('/login');
+ $this->get(route('dashboard'))->assertRedirect(route('login'));
}
public function test_authenticated_users_can_visit_the_dashboard()
{
$this->actingAs($user = User::factory()->create());
- $this->get('/dashboard')->assertOk();
+ $this->get(route('dashboard'))->assertOk();
}
}
diff --git a/tests/Feature/Settings/PasswordUpdateTest.php b/tests/Feature/Settings/PasswordUpdateTest.php
index 64e9189b3..c16652941 100644
--- a/tests/Feature/Settings/PasswordUpdateTest.php
+++ b/tests/Feature/Settings/PasswordUpdateTest.php
@@ -11,14 +11,25 @@ class PasswordUpdateTest extends TestCase
{
use RefreshDatabase;
+ public function test_password_update_page_is_displayed()
+ {
+ $user = User::factory()->create();
+
+ $response = $this
+ ->actingAs($user)
+ ->get(route('password.edit'));
+
+ $response->assertStatus(200);
+ }
+
public function test_password_can_be_updated()
{
$user = User::factory()->create();
$response = $this
->actingAs($user)
- ->from('/settings/password')
- ->put('/settings/password', [
+ ->from(route('password.edit'))
+ ->put(route('password.update'), [
'current_password' => 'password',
'password' => 'new-password',
'password_confirmation' => 'new-password',
@@ -26,7 +37,7 @@ public function test_password_can_be_updated()
$response
->assertSessionHasNoErrors()
- ->assertRedirect('/settings/password');
+ ->assertRedirect(route('password.edit'));
$this->assertTrue(Hash::check('new-password', $user->refresh()->password));
}
@@ -37,8 +48,8 @@ public function test_correct_password_must_be_provided_to_update_password()
$response = $this
->actingAs($user)
- ->from('/settings/password')
- ->put('/settings/password', [
+ ->from(route('password.edit'))
+ ->put(route('password.update'), [
'current_password' => 'wrong-password',
'password' => 'new-password',
'password_confirmation' => 'new-password',
@@ -46,6 +57,6 @@ public function test_correct_password_must_be_provided_to_update_password()
$response
->assertSessionHasErrors('current_password')
- ->assertRedirect('/settings/password');
+ ->assertRedirect(route('password.edit'));
}
}
diff --git a/tests/Feature/Settings/ProfileUpdateTest.php b/tests/Feature/Settings/ProfileUpdateTest.php
index 7d5121441..e6c95cee7 100644
--- a/tests/Feature/Settings/ProfileUpdateTest.php
+++ b/tests/Feature/Settings/ProfileUpdateTest.php
@@ -16,7 +16,7 @@ public function test_profile_page_is_displayed()
$response = $this
->actingAs($user)
- ->get('/settings/profile');
+ ->get(route('profile.edit'));
$response->assertOk();
}
@@ -27,14 +27,14 @@ public function test_profile_information_can_be_updated()
$response = $this
->actingAs($user)
- ->patch('/settings/profile', [
+ ->patch(route('profile.update'), [
'name' => 'Test User',
'email' => 'test@example.com',
]);
$response
->assertSessionHasNoErrors()
- ->assertRedirect('/settings/profile');
+ ->assertRedirect(route('profile.edit'));
$user->refresh();
@@ -49,14 +49,14 @@ public function test_email_verification_status_is_unchanged_when_the_email_addre
$response = $this
->actingAs($user)
- ->patch('/settings/profile', [
+ ->patch(route('profile.update'), [
'name' => 'Test User',
'email' => $user->email,
]);
$response
->assertSessionHasNoErrors()
- ->assertRedirect('/settings/profile');
+ ->assertRedirect(route('profile.edit'));
$this->assertNotNull($user->refresh()->email_verified_at);
}
@@ -67,13 +67,13 @@ public function test_user_can_delete_their_account()
$response = $this
->actingAs($user)
- ->delete('/settings/profile', [
+ ->delete(route('profile.destroy'), [
'password' => 'password',
]);
$response
->assertSessionHasNoErrors()
- ->assertRedirect('/');
+ ->assertRedirect(route('home'));
$this->assertGuest();
$this->assertNull($user->fresh());
@@ -85,14 +85,14 @@ public function test_correct_password_must_be_provided_to_delete_account()
$response = $this
->actingAs($user)
- ->from('/settings/profile')
- ->delete('/settings/profile', [
+ ->from(route('profile.edit'))
+ ->delete(route('profile.destroy'), [
'password' => 'wrong-password',
]);
$response
->assertSessionHasErrors('password')
- ->assertRedirect('/settings/profile');
+ ->assertRedirect(route('profile.edit'));
$this->assertNotNull($user->fresh());
}
diff --git a/tests/Feature/Settings/TwoFactorAuthenticationTest.php b/tests/Feature/Settings/TwoFactorAuthenticationTest.php
new file mode 100644
index 000000000..12dca797c
--- /dev/null
+++ b/tests/Feature/Settings/TwoFactorAuthenticationTest.php
@@ -0,0 +1,92 @@
+markTestSkipped('Two-factor authentication is not enabled.');
+ }
+
+ Features::twoFactorAuthentication([
+ 'confirm' => true,
+ 'confirmPassword' => true,
+ ]);
+
+ $user = User::factory()->create();
+
+ $this->actingAs($user)
+ ->withSession(['auth.password_confirmed_at' => time()])
+ ->get(route('two-factor.show'))
+ ->assertInertia(fn (Assert $page) => $page
+ ->component('settings/two-factor')
+ ->where('twoFactorEnabled', false)
+ );
+ }
+
+ public function test_two_factor_settings_page_requires_password_confirmation_when_enabled()
+ {
+ if (! Features::canManageTwoFactorAuthentication()) {
+ $this->markTestSkipped('Two-factor authentication is not enabled.');
+ }
+
+ $user = User::factory()->create();
+
+ Features::twoFactorAuthentication([
+ 'confirm' => true,
+ 'confirmPassword' => true,
+ ]);
+
+ $response = $this->actingAs($user)
+ ->get(route('two-factor.show'));
+
+ $response->assertRedirect(route('password.confirm'));
+ }
+
+ public function test_two_factor_settings_page_does_not_requires_password_confirmation_when_disabled()
+ {
+ if (! Features::canManageTwoFactorAuthentication()) {
+ $this->markTestSkipped('Two-factor authentication is not enabled.');
+ }
+
+ $user = User::factory()->create();
+
+ Features::twoFactorAuthentication([
+ 'confirm' => true,
+ 'confirmPassword' => false,
+ ]);
+
+ $this->actingAs($user)
+ ->get(route('two-factor.show'))
+ ->assertOk()
+ ->assertInertia(fn (Assert $page) => $page
+ ->component('settings/two-factor')
+ );
+ }
+
+ public function test_two_factor_settings_page_returns_forbidden_response_when_two_factor_is_disabled()
+ {
+ if (! Features::canManageTwoFactorAuthentication()) {
+ $this->markTestSkipped('Two-factor authentication is not enabled.');
+ }
+
+ config(['fortify.features' => []]);
+
+ $user = User::factory()->create();
+
+ $this->actingAs($user)
+ ->withSession(['auth.password_confirmed_at' => time()])
+ ->get(route('two-factor.show'))
+ ->assertForbidden();
+ }
+}
diff --git a/tests/Pest.php b/tests/Pest.php
deleted file mode 100644
index 40d096b52..000000000
--- a/tests/Pest.php
+++ /dev/null
@@ -1,47 +0,0 @@
-extend(Tests\TestCase::class)
- ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
- ->in('Feature');
-
-/*
-|--------------------------------------------------------------------------
-| Expectations
-|--------------------------------------------------------------------------
-|
-| When you're writing tests, you often need to check that values meet certain conditions. The
-| "expect()" function gives you access to a set of "expectations" methods that you can use
-| to assert different things. Of course, you may extend the Expectation API at any time.
-|
-*/
-
-expect()->extend('toBeOne', function () {
- return $this->toBe(1);
-});
-
-/*
-|--------------------------------------------------------------------------
-| Functions
-|--------------------------------------------------------------------------
-|
-| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
-| project that you don't want to repeat in every file. Here you can also expose helpers as
-| global functions to help you to reduce the number of lines of code in your test files.
-|
-*/
-
-function something()
-{
- // ..
-}
diff --git a/tsconfig.json b/tsconfig.json
index cdfd267f8..42777d464 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -109,10 +109,9 @@
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
"baseUrl": ".",
"paths": {
- "@/*": ["./resources/js/*"],
- "ziggy-js": ["./vendor/tightenco/ziggy"]
+ "@/*": ["./resources/js/*"]
},
"jsx": "react-jsx"
},
- "include": ["resources/js/**/*.ts", "resources/js/**/*.tsx"]
+ "include": ["resources/js/**/*.ts", "resources/js/**/*.d.ts", "resources/js/**/*.tsx"]
}
diff --git a/vite.config.js b/vite.config.ts
similarity index 58%
rename from vite.config.js
rename to vite.config.ts
index fc3be2fd9..218357544 100644
--- a/vite.config.js
+++ b/vite.config.ts
@@ -1,21 +1,23 @@
+import { wayfinder } from '@laravel/vite-plugin-wayfinder';
+import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import laravel from 'laravel-vite-plugin';
-import {
- defineConfig
-} from 'vite';
-import tailwindcss from "@tailwindcss/vite";
+import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.tsx'],
- ssr: 'resources/js/ssr.jsx',
+ ssr: 'resources/js/ssr.tsx',
refresh: true,
}),
react(),
tailwindcss(),
+ wayfinder({
+ formVariants: true,
+ }),
],
esbuild: {
jsx: 'automatic',
},
-});
\ No newline at end of file
+});