sed -n '1,260p' tests/Feature/Auth/PasswordResetTest.php && sed -n '1,260p' tests/Feature/Auth/CustomerPasswordResetTest.php

CustomerPasswordResetTest.php

<?php

use App\Models\User;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Support\Facades\Notification;

uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);

test('reset password link screen can be rendered', function () {
    $response = $this->get(route('password.request'));

    $response->assertOk();
});

test('reset password link can be requested', function () {
    Notification::fake();

    $user = User::factory()->create();

    $this->post(route('password.request'), ['email' => $user->email]);

    Notification::assertSentTo($user, ResetPassword::class);
});

test('reset password screen can be rendered', function () {
    Notification::fake();

    $user = User::factory()->create();

    $this->post(route('password.request'), ['email' => $user->email]);

    Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
        $response = $this->get(route('password.reset', $notification->token));
        $response->assertOk();

        return true;
    });
});

test('admin password reset routes render and submit through admin paths', function () {
    Notification::fake();

    $user = User::factory()->create();

    $this->get(route('admin.password.request'))
        ->assertOk()
        ->assertSee(route('admin.password.email', absolute: false), false);

    $this->post(route('admin.password.email'), ['email' => $user->email]);

    Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
        $this->get(route('admin.password.reset', $notification->token).'?email='.$user->email)
            ->assertOk()
            ->assertSee(route('admin.password.update', absolute: false), false);

        $this->post(route('admin.password.update'), [
            'token' => $notification->token,
            'email' => $user->email,
            'password' => 'password',
            'password_confirmation' => 'password',
        ])->assertSessionHasNoErrors();

        return true;
    });
});

test('password can be reset with valid token', function () {
    Notification::fake();

    $user = User::factory()->create();

    $this->post(route('password.request'), ['email' => $user->email]);

    Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
        $response = $this->post(route('password.update'), [
            'token' => $notification->token,
            'email' => $user->email,
            'password' => 'password',
            'password_confirmation' => 'password',
        ]);

        $response
            ->assertSessionHasNoErrors()
            ->assertRedirect(route('login', absolute: false));

        return true;
    });
});
<?php

use App\Livewire\Storefront\Account\Auth\ForgotPassword as CustomerForgotPassword;
use App\Livewire\Storefront\Account\Auth\ResetPassword as CustomerResetPassword;
use App\Models\Customer;
use App\Models\Store;
use App\Models\StoreDomain;
use App\Notifications\CustomerResetPassword as CustomerResetPasswordNotification;
use App\Services\CustomerPasswordResetService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Notification;
use Livewire\Livewire;

uses(RefreshDatabase::class);

beforeEach(function (): void {
    $this->withoutVite();
    Cache::flush();
});

test('customer password reset pages render for the resolved storefront store', function (): void {
    $store = Store::factory()->create();
    StoreDomain::factory()->create([
        'store_id' => $store->getKey(),
        'hostname' => 'customer-reset.test',
    ]);

    $this->get('http://customer-reset.test/account/forgot-password')
        ->assertSuccessful()
        ->assertSee('Reset password')
        ->assertSee('Send reset link');

    $this->get('http://customer-reset.test/account/reset-password/test-token?email=customer@example.test')
        ->assertSuccessful()
        ->assertSee('Set a new password')
        ->assertSee('Reset password');
});

test('customer reset links are sent generically and scoped to the current store', function (): void {
    Notification::fake();

    $store = Store::factory()->create();
    $otherStore = Store::factory()->create();

    $customer = Customer::factory()->create([
        'store_id' => $store->getKey(),
        'email' => 'customer@example.test',
    ]);

    $otherCustomer = Customer::factory()->create([
        'store_id' => $otherStore->getKey(),
        'email' => 'customer@example.test',
    ]);

    app()->instance('current_store', $store);

    Livewire::test(CustomerForgotPassword::class)
        ->set('email', 'CUSTOMER@example.test')
        ->call('send')
        ->assertHasNoErrors()
        ->assertSee('If an account matches that email, a reset link has been sent.');

    Notification::assertSentTo(
        $customer,
        CustomerResetPasswordNotification::class,
        fn (CustomerResetPasswordNotification $notification): bool => strlen($notification->token) === 64
            && $notification->store->is($store),
    );

    Notification::assertNotSentTo($otherCustomer, CustomerResetPasswordNotification::class);

    $this->assertDatabaseHas('customer_password_reset_tokens', [
        'store_id' => $store->getKey(),
        'email' => $customer->email,
    ]);

    $this->assertDatabaseMissing('customer_password_reset_tokens', [
        'store_id' => $otherStore->getKey(),
        'email' => $otherCustomer->email,
    ]);
});

test('unknown customer reset requests keep the generic response', function (): void {
    Notification::fake();

    $store = Store::factory()->create();

    app()->instance('current_store', $store);

    Livewire::test(CustomerForgotPassword::class)
        ->set('email', 'missing@example.test')
        ->call('send')
        ->assertHasNoErrors()
        ->assertSee('If an account matches that email, a reset link has been sent.');

    Notification::assertNothingSent();

    expect(DB::table('customer_password_reset_tokens')->count())->toBe(0);
});

test('customer password reset updates the password and deletes the token', function (): void {
    Notification::fake();

    $store = Store::factory()->create();
    $customer = Customer::factory()->create([
        'store_id' => $store->getKey(),
        'email' => 'customer@example.test',
        'password' => 'old-password',
    ]);

    app()->instance('current_store', $store);

    app(CustomerPasswordResetService::class)->sendResetLink($store, $customer->email);

    $token = null;

    Notification::assertSentTo(
        $customer,
        CustomerResetPasswordNotification::class,
        function (CustomerResetPasswordNotification $notification) use (&$token): bool {
            $token = $notification->token;

            return true;
        },
    );

    Livewire::test(CustomerResetPassword::class, ['token' => $token])
        ->set('email', $customer->email)
        ->set('password', 'new-password')
        ->set('password_confirmation', 'new-password')
        ->call('resetPassword')
        ->assertHasNoErrors()
        ->assertRedirect(route('account.login', absolute: false));

    expect(Hash::check('new-password', $customer->refresh()->password_hash))->toBeTrue();

    $this->assertDatabaseMissing('customer_password_reset_tokens', [
        'store_id' => $store->getKey(),
        'email' => $customer->email,
    ]);

    app()->instance('current_store', $store);

    expect(Auth::guard('customer')->attempt([
        'email' => $customer->email,
        'password' => 'new-password',
    ]))->toBeTrue();
});

test('customer reset tokens cannot be reused across stores or after expiry', function (): void {
    $store = Store::factory()->create();
    $otherStore = Store::factory()->create();

    $customer = Customer::factory()->create([
        'store_id' => $store->getKey(),
        'email' => 'shared@example.test',
        'password' => 'old-password',
    ]);

    $otherCustomer = Customer::factory()->create([
        'store_id' => $otherStore->getKey(),
        'email' => 'shared@example.test',
        'password' => 'other-password',
    ]);

    DB::table('customer_password_reset_tokens')->insert([
        'store_id' => $store->getKey(),
        'email' => $customer->email,
        'token' => Hash::make('valid-token'),
        'created_at' => now(),
    ]);

    app()->instance('current_store', $otherStore);

    Livewire::test(CustomerResetPassword::class, ['token' => 'valid-token'])
        ->set('email', $otherCustomer->email)
        ->set('password', 'new-password')
        ->set('password_confirmation', 'new-password')
        ->call('resetPassword')
        ->assertHasErrors(['email']);

    expect(Hash::check('other-password', $otherCustomer->refresh()->password_hash))->toBeTrue();

    DB::table('customer_password_reset_tokens')
        ->where('store_id', $store->getKey())
        ->where('email', $customer->email)
        ->update(['created_at' => now()->subMinutes(61)]);

    app()->instance('current_store', $store);

    Livewire::test(CustomerResetPassword::class, ['token' => 'valid-token'])
        ->set('email', $customer->email)
        ->set('password', 'new-password')
        ->set('password_confirmation', 'new-password')
        ->call('resetPassword')
        ->assertHasErrors(['email']);

    expect(Hash::check('old-password', $customer->refresh()->password_hash))->toBeTrue();
});


sed -n '1,160p' app/Providers/FortifyServiceProvider.php && sed -n '1,140p' config/fortify.php

FortifyServiceProvider.php

<?php

namespace App\Providers;

use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Laravel\Fortify\Fortify;

class FortifyServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        //
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        $this->configureActions();
        $this->configureViews();
        $this->configureRateLimiting();
    }

    /**
     * Configure Fortify actions.
     */
    private function configureActions(): void
    {
        Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
        Fortify::createUsersUsing(CreateNewUser::class);
    }

    /**
     * Configure Fortify views.
     */
    private function configureViews(): void
    {
        Fortify::loginView(fn () => view('livewire.auth.login'));
        Fortify::verifyEmailView(fn () => view('livewire.auth.verify-email'));
        Fortify::twoFactorChallengeView(fn () => view('livewire.auth.two-factor-challenge'));
        Fortify::confirmPasswordView(fn () => view('livewire.auth.confirm-password'));
        Fortify::registerView(fn () => view('livewire.auth.register'));
        Fortify::resetPasswordView(fn () => view('livewire.auth.reset-password'));
        Fortify::requestPasswordResetLinkView(fn () => view('livewire.auth.forgot-password'));
    }

    /**
     * Configure rate limiting.
     */
    private function configureRateLimiting(): void
    {
        RateLimiter::for('two-factor', function (Request $request) {
            return Limit::perMinute(5)->by($request->session()->get('login.id'));
        });

        RateLimiter::for('login', function (Request $request) {
            return Limit::perMinute(5)->by($request->ip());
        });
    }
}
<?php

use Laravel\Fortify\Features;

return [

    /*
    |--------------------------------------------------------------------------
    | Fortify Guard
    |--------------------------------------------------------------------------
    |
    | Here you may specify which authentication guard Fortify will use while
    | authenticating users. This value should correspond with one of your
    | guards that is already present in your "auth" configuration file.
    |
    */

    'guard' => 'web',

    /*
    |--------------------------------------------------------------------------
    | Fortify Password Broker
    |--------------------------------------------------------------------------
    |
    | Here you may specify which password broker Fortify can use when a user
    | is resetting their password. This configured value should match one
    | of your password brokers setup in your "auth" configuration file.
    |
    */

    'passwords' => 'users',

    /*
    |--------------------------------------------------------------------------
    | Username / Email
    |--------------------------------------------------------------------------
    |
    | This value defines which model attribute should be considered as your
    | application's "username" field. Typically, this might be the email
    | address of the users but you are free to change this value here.
    |
    | Out of the box, Fortify expects forgot password and reset password
    | requests to have a field named 'email'. If the application uses
    | another name for the field you may define it below as needed.
    |
    */

    'username' => 'email',

    'email' => 'email',

    /*
    |--------------------------------------------------------------------------
    | Lowercase Usernames
    |--------------------------------------------------------------------------
    |
    | This value defines whether usernames should be lowercased before saving
    | them in the database, as some database system string fields are case
    | sensitive. You may disable this for your application if necessary.
    |
    */

    'lowercase_usernames' => true,

    /*
    |--------------------------------------------------------------------------
    | Home Path
    |--------------------------------------------------------------------------
    |
    | Here you may configure the path where users will get redirected during
    | authentication or password reset when the operations are successful
    | and the user is authenticated. You are free to change this value.
    |
    */

    'home' => '/admin',

    /*
    |--------------------------------------------------------------------------
    | Fortify Routes Prefix / Subdomain
    |--------------------------------------------------------------------------
    |
    | Here you may specify which prefix Fortify will assign to all the routes
    | that it registers with the application. If necessary, you may change
    | subdomain under which all of the Fortify routes will be available.
    |
    */

    'prefix' => '',

    'domain' => null,

    /*
    |--------------------------------------------------------------------------
    | Fortify Routes Middleware
    |--------------------------------------------------------------------------
    |
    | Here you may specify which middleware Fortify will assign to the routes
    | that it registers with the application. If necessary, you may change
    | these middleware but typically this provided default is preferred.
    |
    */

    'middleware' => ['web'],

    /*
    |--------------------------------------------------------------------------
    | Rate Limiting
    |--------------------------------------------------------------------------
    |
    | By default, Fortify will throttle logins to five requests per minute for
    | every email and IP address combination. However, if you would like to
    | specify a custom rate limiter to call then you may specify it here.
    |
    */

    'limiters' => [
        'login' => 'login',
        'two-factor' => 'two-factor',
    ],

    /*
    |--------------------------------------------------------------------------
    | Register View Routes
    |--------------------------------------------------------------------------
    |
    | Here you may specify if the routes returning views should be disabled as
    | you may not need them when building your own application. This may be
    | especially true if you're writing a custom single-page application.
    |
    */

    'views' => true,

    /*
    |--------------------------------------------------------------------------
    | Features
    |--------------------------------------------------------------------------
    |
    | Some of the Fortify features are optional. You may disable the features


Output:
Chunk ID: fa6ac1
Wall time: 0.1902 seconds
Process exited with code 0
Original token count: 21
Output:

   ERROR  Your application doesn't have any routes matching the given criteria.


Output:
Chunk ID: 5f68e9
Wall time: 0.1812 seconds
Process exited with code 0
Original token count: 21
Output:

   ERROR  Your application doesn't have any routes matching the given criteria.


Output:
Chunk ID: eb969e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2387
Output:
<?php

use App\Models\User;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Support\Facades\Notification;

uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);

test('reset password link screen can be rendered', function () {
    $response = $this->get(route('password.request'));

    $response->assertOk();
});

test('reset password link can be requested', function () {
    Notification::fake();

    $user = User::factory()->create();

    $this->post(route('password.request'), ['email' => $user->email]);

    Notification::assertSentTo($user, ResetPassword::class);
});

test('reset password screen can be rendered', function () {
    Notification::fake();

    $user = User::factory()->create();

    $this->post(route('password.request'), ['email' => $user->email]);

    Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
        $response = $this->get(route('password.reset', $notification->token));
        $response->assertOk();

        return true;
    });
});

test('admin password reset routes render and submit through admin paths', function () {
    Notification::fake();

    $user = User::factory()->create();

    $this->get(route('admin.password.request'))
        ->assertOk()
        ->assertSee(route('admin.password.email', absolute: false), false);

    $this->post(route('admin.password.email'), ['email' => $user->email]);

    Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
        $this->get(route('admin.password.reset', $notification->token).'?email='.$user->email)
            ->assertOk()
            ->assertSee(route('admin.password.update', absolute: false), false);

        $this->post(route('admin.password.update'), [
            'token' => $notification->token,
            'email' => $user->email,
            'password' => 'password',
            'password_confirmation' => 'password',
        ])->assertSessionHasNoErrors();

        return true;
    });
});

test('password can be reset with valid token', function () {
    Notification::fake();

    $user = User::factory()->create();

    $this->post(route('password.request'), ['email' => $user->email]);

    Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
        $response = $this->post(route('password.update'), [
            'token' => $notification->token,
            'email' => $user->email,
            'password' => 'password',
            'password_confirmation' => 'password',
        ]);

        $response
            ->assertSessionHasNoErrors()
            ->assertRedirect(route('login', absolute: false));

        return true;
    });
});
<?php

use App\Livewire\Storefront\Account\Auth\ForgotPassword as CustomerForgotPassword;
use App\Livewire\Storefront\Account\Auth\ResetPassword as CustomerResetPassword;
use App\Models\Customer;
use App\Models\Store;
use App\Models\StoreDomain;
use App\Notifications\CustomerResetPassword as CustomerResetPasswordNotification;
use App\Services\CustomerPasswordResetService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Notification;
use Livewire\Livewire;

uses(RefreshDatabase::class);

beforeEach(function (): void {
    $this->withoutVite();
    Cache::flush();
});

test('customer password reset pages render for the resolved storefront store', function (): void {
    $store = Store::factory()->create();
    StoreDomain::factory()->create([
        'store_id' => $store->getKey(),
        'hostname' => 'customer-reset.test',
    ]);

    $this->get('http://customer-reset.test/account/forgot-password')
        ->assertSuccessful()
        ->assertSee('Reset password')
        ->assertSee('Send reset link');

    $this->get('http://customer-reset.test/account/reset-password/test-token?email=customer@example.test')
        ->assertSuccessful()
        ->assertSee('Set a new password')
        ->assertSee('Reset password');
});

test('customer reset links are sent generically and scoped to the current store', function (): void {
    Notification::fake();

    $store = Store::factory()->create();
    $otherStore = Store::factory()->create();

    $customer = Customer::factory()->create([
        'store_id' => $store->getKey(),
        'email' => 'customer@example.test',
    ]);

    $otherCustomer = Customer::factory()->create([
        'store_id' => $otherStore->getKey(),
        'email' => 'customer@example.test',
    ]);

    app()->instance('current_store', $store);

    Livewire::test(CustomerForgotPassword::class)
        ->set('email', 'CUSTOMER@example.test')
        ->call('send')
        ->assertHasNoErrors()
        ->assertSee('If an account matches that email, a reset link has been sent.');

    Notification::assertSentTo(
        $customer,
        CustomerResetPasswordNotification::class,
        fn (CustomerResetPasswordNotification $notification): bool => strlen($notification->token) === 64
            && $notification->store->is($store),
    );

    Notification::assertNotSentTo($otherCustomer, CustomerResetPasswordNotification::class);

    $this->assertDatabaseHas('customer_password_reset_tokens', [
        'store_id' => $store->getKey(),
        'email' => $customer->email,
    ]);

    $this->assertDatabaseMissing('customer_password_reset_tokens', [
        'store_id' => $otherStore->getKey(),
        'email' => $otherCustomer->email,
    ]);
});

test('unknown customer reset requests keep the generic response', function (): void {
    Notification::fake();

    $store = Store::factory()->create();

    app()->instance('current_store', $store);

    Livewire::test(CustomerForgotPassword::class)
        ->set('email', 'missing@example.test')
        ->call('send')
        ->assertHasNoErrors()
        ->assertSee('If an account matches that email, a reset link has been sent.');

    Notification::assertNothingSent();

    expect(DB::table('customer_password_reset_tokens')->count())->toBe(0);
});

test('customer password reset updates the password and deletes the token', function (): void {
    Notification::fake();

    $store = Store::factory()->create();
    $customer = Customer::factory()->create([
        'store_id' => $store->getKey(),
        'email' => 'customer@example.test',
        'password' => 'old-password',
    ]);

    app()->instance('current_store', $store);

    app(CustomerPasswordResetService::class)->sendResetLink($store, $customer->email);

    $token = null;

    Notification::assertSentTo(
        $customer,
        CustomerResetPasswordNotification::class,
        function (CustomerResetPasswordNotification $notification) use (&$token): bool {
            $token = $notification->token;

            return true;
        },
    );

    Livewire::test(CustomerResetPassword::class, ['token' => $token])
        ->set('email', $customer->email)
        ->set('password', 'new-password')
        ->set('password_confirmation', 'new-password')
        ->call('resetPassword')
        ->assertHasNoErrors()
        ->assertRedirect(route('account.login', absolute: false));

    expect(Hash::check('new-password', $customer->refresh()->password_hash))->toBeTrue();

    $this->assertDatabaseMissing('customer_password_reset_tokens', [
        'store_id' => $store->getKey(),
        'email' => $customer->email,
    ]);

    app()->instance('current_store', $store);

    expect(Auth::guard('customer')->attempt([
        'email' => $customer->email,
        'password' => 'new-password',
    ]))->toBeTrue();
});

test('customer reset tokens cannot be reused across stores or after expiry', function (): void {
    $store = Store::factory()->create();
    $otherStore = Store::factory()->create();

    $customer = Customer::factory()->create([
        'store_id' => $store->getKey(),
        'email' => 'shared@example.test',
        'password' => 'old-password',
    ]);

    $otherCustomer = Customer::factory()->create([
        'store_id' => $otherStore->getKey(),
        'email' => 'shared@example.test',
        'password' => 'other-password',
    ]);

    DB::table('customer_password_reset_tokens')->insert([
        'store_id' => $store->getKey(),
        'email' => $customer->email,
        'token' => Hash::make('valid-token'),
        'created_at' => now(),
    ]);

    app()->instance('current_store', $otherStore);

    Livewire::test(CustomerResetPassword::class, ['token' => 'valid-token'])
        ->set('email', $otherCustomer->email)
        ->set('password', 'new-password')
        ->set('password_confirmation', 'new-password')
        ->call('resetPassword')
        ->assertHasErrors(['email']);

    expect(Hash::check('other-password', $otherCustomer->refresh()->password_hash))->toBeTrue();

    DB::table('customer_password_reset_tokens')
        ->where('store_id', $store->getKey())
        ->where('email', $customer->email)
        ->update(['created_at' => now()->subMinutes(61)]);

    app()->instance('current_store', $store);

    Livewire::test(CustomerResetPassword::class, ['token' => 'valid-token'])
        ->set('email', $customer->email)
        ->set('password', 'new-password')
        ->set('password_confirmation', 'new-password')
        ->call('resetPassword')
        ->assertHasErrors(['email']);

    expect(Hash::check('old-password', $customer->refresh()->password_hash))->toBeTrue();
});


Output:
Chunk ID: a69a71
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1711
Output:
<?php

namespace App\Providers;

use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Laravel\Fortify\Fortify;

class FortifyServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        //
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        $this->configureActions();
        $this->configureViews();
        $this->configureRateLimiting();
    }

    /**
     * Configure Fortify actions.
     */
    private function configureActions(): void
    {
        Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
        Fortify::createUsersUsing(CreateNewUser::class);
    }

    /**
     * Configure Fortify views.
     */
    private function configureViews(): void
    {
        Fortify::loginView(fn () => view('livewire.auth.login'));
        Fortify::verifyEmailView(fn () => view('livewire.auth.verify-email'));
        Fortify::twoFactorChallengeView(fn () => view('livewire.auth.two-factor-challenge'));
        Fortify::confirmPasswordView(fn () => view('livewire.auth.confirm-password'));
        Fortify::registerView(fn () => view('livewire.auth.register'));
        Fortify::resetPasswordView(fn () => view('livewire.auth.reset-password'));
        Fortify::requestPasswordResetLinkView(fn () => view('livewire.auth.forgot-password'));
    }

    /**
     * Configure rate limiting.
     */
    private function configureRateLimiting(): void
    {
        RateLimiter::for('two-factor', function (Request $request) {
            return Limit::perMinute(5)->by($request->session()->get('login.id'));
        });

        RateLimiter::for('login', function (Request $request) {
            return Limit::perMinute(5)->by($request->ip());
        });
    }
}
<?php

use Laravel\Fortify\Features;

return [

    /*
    |--------------------------------------------------------------------------
    | Fortify Guard
    |--------------------------------------------------------------------------
    |
    | Here you may specify which authentication guard Fortify will use while
    | authenticating users. This value should correspond with one of your
    | guards that is already present in your "auth" configuration file.
    |
    */

    'guard' => 'web',

    /*
    |--------------------------------------------------------------------------
    | Fortify Password Broker
    |--------------------------------------------------------------------------
    |
    | Here you may specify which password broker Fortify can use when a user
    | is resetting their password. This configured value should match one
    | of your password brokers setup in your "auth" configuration file.
    |
    */

    'passwords' => 'users',

    /*
    |--------------------------------------------------------------------------
    | Username / Email
    |--------------------------------------------------------------------------
    |
    | This value defines which model attribute should be considered as your
    | application's "username" field. Typically, this might be the email
    | address of the users but you are free to change this value here.
    |
    | Out of the box, Fortify expects forgot password and reset password
    | requests to have a field named 'email'. If the application uses
    | another name for the field you may define it below as needed.
    |
    */

    'username' => 'email',

    'email' => 'email',

    /*
    |--------------------------------------------------------------------------
    | Lowercase Usernames
    |--------------------------------------------------------------------------
    |
    | This value defines whether usernames should be lowercased before saving
    | them in the database, as some database system string fields are case
    | sensitive. You may disable this for your application if necessary.
    |
    */

    'lowercase_usernames' => true,

    /*
    |--------------------------------------------------------------------------
    | Home Path
    |--------------------------------------------------------------------------
    |
    | Here you may configure the path where users will get redirected during
    | authentication or password reset when the operations are successful
    | and the user is authenticated. You are free to change this value.
    |
    */

    'home' => '/admin',

    /*
    |--------------------------------------------------------------------------
    | Fortify Routes Prefix / Subdomain
    |--------------------------------------------------------------------------
    |
    | Here you may specify which prefix Fortify will assign to all the routes
    | that it registers with the application. If necessary, you may change
    | subdomain under which all of the Fortify routes will be available.
    |
    */

    'prefix' => '',

    'domain' => null,

    /*
    |--------------------------------------------------------------------------
    | Fortify Routes Middleware
    |--------------------------------------------------------------------------
    |
    | Here you may specify which middleware Fortify will assign to the routes
    | that it registers with the application. If necessary, you may change
    | these middleware but typically this provided default is preferred.
    |
    */

    'middleware' => ['web'],

    /*
    |--------------------------------------------------------------------------
    | Rate Limiting
    |--------------------------------------------------------------------------
    |
    | By default, Fortify will throttle logins to five requests per minute for
    | every email and IP address combination. However, if you would like to
    | specify a custom rate limiter to call then you may specify it here.
    |
    */

    'limiters' => [
        'login' => 'login',
        'two-factor' => 'two-factor',
    ],

    /*
    |--------------------------------------------------------------------------
    | Register View Routes
    |--------------------------------------------------------------------------
    |
    | Here you may specify if the routes returning views should be disabled as
    | you may not need them when building your own application. This may be
    | especially true if you're writing a custom single-page application.
    |
    */

    'views' => true,

    /*
    |--------------------------------------------------------------------------
    | Features
    |--------------------------------------------------------------------------
    |
    | Some of the Fortify features are optional. You may disable the features


php artisan route:list --path=reset-password --except-vendor

ERROR  Your application doesn't have any routes matching the given criteria.


php artisan route:list --path=forgot-password --except-vendor


sed -n '140,280p' config/fortify.php && php artisan route:list --name=password --except-vendor


Output:
Chunk ID: bd1066
Wall time: 0.0571 seconds
Process exited with code 0
Original token count: 155
Output:
    | Some of the Fortify features are optional. You may disable the features
    | by removing them from this array. You're free to only remove some of
    | these features, or you can even remove all of these if you need to.
    |
    */

    'features' => [
        Features::registration(),
        Features::resetPasswords(),
        Features::emailVerification(),
        Features::twoFactorAuthentication([
            'confirm' => true,
            'confirmPassword' => true,
            // 'window' => 0
        ]),
    ],

];

   ERROR  Your application doesn't have any routes matching the given criteria.


php artisan route:list --path=forgot-password && php artisan route:list --path=reset-password && php artisan route:list --name=password


Output:
Chunk ID: 12ed32
Wall time: 0.3141 seconds
Process exited with code 0
Original token count: 567
Output:

  GET|HEAD   account/forgot-password account.password.request › Livewire\Mech…
  GET|HEAD   admin/forgot-password admin.password.request › Laravel\Fortify  …
  POST       admin/forgot-password admin.password.email › Laravel\Fortify › P…
  GET|HEAD   forgot-password password.request › Laravel\Fortify › PasswordRes…
  POST       forgot-password password.email › Laravel\Fortify › PasswordReset…

                                                            Showing [5] routes

  GET|HEAD   account/reset-password/{token} account.password.reset › Livewire…
  POST       admin/reset-password admin.password.update › Laravel\Fortify › N…
  GET|HEAD   admin/reset-password/{token} admin.password.reset › Laravel\Fort…
  POST       reset-password password.update › Laravel\Fortify › NewPasswordCo…
  GET|HEAD   reset-password/{token} password.reset › Laravel\Fortify › NewPas…

                                                            Showing [5] routes

  GET|HEAD   account/forgot-password account.password.request › Livewire\Mech…
  GET|HEAD   account/reset-password/{token} account.password.reset › Livewire…
  GET|HEAD   admin/forgot-password admin.password.request › Laravel\Fortify  …
  POST       admin/forgot-password admin.password.email › Laravel\Fortify › P…
  POST       admin/reset-password admin.password.update › Laravel\Fortify › N…
  GET|HEAD   admin/reset-password/{token} admin.password.reset › Laravel\Fort…
  GET|HEAD   forgot-password password.request › Laravel\Fortify › PasswordRes…
  POST       forgot-password password.email › Laravel\Fortify › PasswordReset…
  POST       reset-password password.update › Laravel\Fortify › NewPasswordCo…
  GET|HEAD   reset-password/{token} password.reset › Laravel\Fortify › NewPas…
  GET|HEAD   settings/password user-password.edit › Livewire\Mechanisms › Liv…
  GET|HEAD   user/confirm-password password.confirm › Laravel\Fortify › Confi…
  POST       user/confirm-password password.confirm.store › Laravel\Fortify  …
  GET|HEAD   user/confirmed-password-status password.confirmation › Laravel\F…

                                                           Showing [14] routes


php artisan route:list --path=forgot-password && php artisan route:list --path=reset-password && php artisan route:list --name=password

  GET|HEAD   account/forgot-password account.password.request › Livewire\Mech…
  GET|HEAD   admin/forgot-password admin.password.request › Laravel\Fortify  …
  POST       admin/forgot-password admin.password.email › Laravel\Fortify › P…
  GET|HEAD   forgot-password password.request › Laravel\Fortify › PasswordRes…
  POST       forgot-password password.email › Laravel\Fortify › PasswordReset…

                                                            Showing [5] routes

  GET|HEAD   account/reset-password/{token} account.password.reset › Livewire…
  POST       admin/reset-password admin.password.update › Laravel\Fortify › N…
  GET|HEAD   admin/reset-password/{token} admin.password.reset › Laravel\Fort…
  POST       reset-password password.update › Laravel\Fortify › NewPasswordCo…
  GET|HEAD   reset-password/{token} password.reset › Laravel\Fortify › NewPas…

                                                            Showing [5] routes

  GET|HEAD   account/forgot-password account.password.request › Livewire\Mech…
  GET|HEAD   account/reset-password/{token} account.password.reset › Livewire…
  GET|HEAD   admin/forgot-password admin.password.request › Laravel\Fortify  …
  POST       admin/forgot-password admin.password.email › Laravel\Fortify › P…
  POST       admin/reset-password admin.password.update › Laravel\Fortify › N…
  GET|HEAD   admin/reset-password/{token} admin.password.reset › Laravel\Fort…
  GET|HEAD   forgot-password password.request › Laravel\Fortify › PasswordRes…
  POST       forgot-password password.email › Laravel\Fortify › PasswordReset…
  POST       reset-password password.update › Laravel\Fortify › NewPasswordCo…
  GET|HEAD   reset-password/{token} password.reset › Laravel\Fortify › NewPas…
  GET|HEAD   settings/password user-password.edit › Livewire\Mechanisms › Liv…
  GET|HEAD   user/confirm-password password.confirm › Laravel\Fortify › Confi…
  POST       user/confirm-password password.confirm.store › Laravel\Fortify  …
  GET|HEAD   user/confirmed-password-status password.confirmation › Laravel\F…

                                                           Showing [14] routes


rg -n "forgot-password|reset-password|password reset|Password reset|password\.request|account/forgot|account/reset|/forgot|/reset" specs/02-API-ROUTES.md specs/04-STOREFRONT-UI.md specs/06-AUTH-AND-SECURITY.md specs/09-IMPLEMENTATION-ROADMAP.md


rg -n "forgot-password|reset-password|password reset|Password reset|password\.request|account/forgot|account/reset|/forgot|/reset" specs/02-API-ROUTES.md specs/04-STOREFRONT-UI.md specs/06-AUTH-AND-SECURITY.md specs/09-IMPLEMENTATION-ROADMAP.md

rg -n "forgot-password|reset-password|password reset|Password reset|password\\.request|account/forgot|account/reset|/forgot|/reset" specs/02-API-ROUTES.md specs/04-STOREFRONT-UI.md specs/06-AUTH-AND-SECURITY.md specs/09-IMPLEMENTATION-ROADMAP.md

Question: forgot-password|reset-password|password reset|Password reset|password\.request|account/forgot|account/reset|/forgot|/reset

specs/04-STOREFRONT-UI.md:1921:| Password reset sent | "We have emailed your password reset link!" | success |
specs/06-AUTH-AND-SECURITY.md:75:    B->>App: GET /admin/forgot-password
specs/06-AUTH-AND-SECURITY.md:76:    App-->>B: Render forgot-password form
specs/06-AUTH-AND-SECURITY.md:77:    B->>App: POST /admin/forgot-password (email)
specs/06-AUTH-AND-SECURITY.md:84:    B->>App: GET /admin/reset-password/{token}
specs/06-AUTH-AND-SECURITY.md:85:    App-->>B: Render reset-password form
specs/06-AUTH-AND-SECURITY.md:86:    B->>App: POST /admin/reset-password (token, email, password, password_confirmation)
specs/06-AUTH-AND-SECURITY.md:199:- Routes: `GET /forgot-password`, `POST /forgot-password`, `GET /reset-password/{token}`, `POST /reset-password`.
specs/06-AUTH-AND-SECURITY.md:611:| Admin web (auth pages) | `web` | Login, forgot-password, reset-password |
specs/02-API-ROUTES.md:20:| GET | `/admin/forgot-password` | Show forgot password form | `Livewire: Admin\Auth\ForgotPassword` |
specs/02-API-ROUTES.md:21:| POST | `/admin/forgot-password` | Send password reset link | Handled by Livewire action |
specs/02-API-ROUTES.md:22:| GET | `/admin/reset-password/{token}` | Show reset password form | `Livewire: Admin\Auth\ResetPassword` |
specs/02-API-ROUTES.md:23:| POST | `/admin/reset-password` | Reset password with token | Handled by Livewire action |
specs/02-API-ROUTES.md:179:| GET | `/forgot-password` | Forgot password page | `Storefront\Account\Auth\ForgotPassword` |
specs/02-API-ROUTES.md:180:| POST | `/forgot-password` | Send reset link | Handled by Livewire action |
specs/02-API-ROUTES.md:181:| GET | `/reset-password/{token}` | Reset password form | `Storefront\Account\Auth\ResetPassword` |
specs/02-API-ROUTES.md:182:| POST | `/reset-password` | Reset password | Handled by Livewire action |
specs/09-IMPLEMENTATION-ROADMAP.md:96:| `config/auth.php` | Passwords | Add `customers` password broker | Customer password reset |
specs/09-IMPLEMENTATION-ROADMAP.md:188:| Password reset | Standard Laravel flow with `Password::broker('users')` |


Output:
Chunk ID: 9b1d56
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 519
Output:
specs/04-STOREFRONT-UI.md:1921:| Password reset sent | "We have emailed your password reset link!" | success |
specs/06-AUTH-AND-SECURITY.md:75:    B->>App: GET /admin/forgot-password
specs/06-AUTH-AND-SECURITY.md:76:    App-->>B: Render forgot-password form
specs/06-AUTH-AND-SECURITY.md:77:    B->>App: POST /admin/forgot-password (email)
specs/06-AUTH-AND-SECURITY.md:84:    B->>App: GET /admin/reset-password/{token}
specs/06-AUTH-AND-SECURITY.md:85:    App-->>B: Render reset-password form
specs/06-AUTH-AND-SECURITY.md:86:    B->>App: POST /admin/reset-password (token, email, password, password_confirmation)
specs/06-AUTH-AND-SECURITY.md:199:- Routes: `GET /forgot-password`, `POST /forgot-password`, `GET /reset-password/{token}`, `POST /reset-password`.
specs/06-AUTH-AND-SECURITY.md:611:| Admin web (auth pages) | `web` | Login, forgot-password, reset-password |
specs/02-API-ROUTES.md:20:| GET | `/admin/forgot-password` | Show forgot password form | `Livewire: Admin\Auth\ForgotPassword` |
specs/02-API-ROUTES.md:21:| POST | `/admin/forgot-password` | Send password reset link | Handled by Livewire action |
specs/02-API-ROUTES.md:22:| GET | `/admin/reset-password/{token}` | Show reset password form | `Livewire: Admin\Auth\ResetPassword` |
specs/02-API-ROUTES.md:23:| POST | `/admin/reset-password` | Reset password with token | Handled by Livewire action |
specs/02-API-ROUTES.md:179:| GET | `/forgot-password` | Forgot password page | `Storefront\Account\Auth\ForgotPassword` |
specs/02-API-ROUTES.md:180:| POST | `/forgot-password` | Send reset link | Handled by Livewire action |
specs/02-API-ROUTES.md:181:| GET | `/reset-password/{token}` | Reset password form | `Storefront\Account\Auth\ResetPassword` |
specs/02-API-ROUTES.md:182:| POST | `/reset-password` | Reset password | Handled by Livewire action |
specs/09-IMPLEMENTATION-ROADMAP.md:96:| `config/auth.php` | Passwords | Add `customers` password broker | Customer password reset |
specs/09-IMPLEMENTATION-ROADMAP.md:188:| Password reset | Standard Laravel flow with `Password::broker('users')` |


sed -n '60,220p' specs/06-AUTH-AND-SECURITY.md && sed -n '1,40p' specs/02-API-ROUTES.md && sed -n '160,190p' specs/02-API-ROUTES.md && sed -n '1800,1940p' specs/04-STOREFRONT-UI.md


sed -n '60,220p' specs/06-AUTH-AND-SECURITY.md && sed -n '1,40p' specs/02-API-ROUTES.md && sed -n '160,190p' specs/02-API-ROUTES.md && sed -n '1800,1940p' specs/04-STOREFRONT-UI.md

- An optional "Remember me" checkbox on the login form.
- When enabled, Laravel sets a long-lived remember token cookie.
- The `remember_token` column on the `users` table stores the hashed token.
- Cookie name follows Laravel default convention: `remember_web_{hash}`.

#### Admin Password Reset Flow

```mermaid
sequenceDiagram
    participant B as Browser
    participant App as Application
    participant DB as Database
    participant Mail as Mail Service

    B->>App: GET /admin/forgot-password
    App-->>B: Render forgot-password form
    B->>App: POST /admin/forgot-password (email)
    App->>DB: Look up user by email
    alt User exists
        App->>DB: Store reset token in password_reset_tokens table
        App->>Mail: Send reset link email
    end
    App-->>B: "If that email exists, we sent a reset link." (generic)
    B->>App: GET /admin/reset-password/{token}
    App-->>B: Render reset-password form
    B->>App: POST /admin/reset-password (token, email, password, password_confirmation)
    App->>DB: Validate token (exists, not expired, matches email)
    alt Valid token
        App->>DB: Update user password (bcrypt hash)
        App->>DB: Delete used token
        App-->>B: Redirect to /admin/login with success flash
    else Invalid/expired token
        App-->>B: Error message
    end
```

**Requirements:**

- Uses Laravel's built-in password broker (`users` broker).
- Reset tokens stored in the `password_reset_tokens` table.
- Token expiration: 60 minutes (configurable in `config/auth.php` under `passwords.users.expire`).
- Throttle: one reset email per 60 seconds per email address.
- The "forgot password" response must be generic (do not reveal whether email exists).

#### Admin Logout

- Route: `POST /admin/logout` (via Livewire action or standard route).
- Steps: invalidate the entire session, regenerate the CSRF token, redirect to `/admin/login`.
- The response must not cache.

#### Session Lifetime

| Setting | Value |
|---------|-------|
| Lifetime | 120 minutes (configurable via `SESSION_LIFETIME` env var) |
| Expire on close | No (session persists until lifetime expires) |

---

### 1.2 Customer Authentication

| Setting | Value |
|---------|-------|
| Guard name | `customer` (custom guard) |
| Provider name | `customers` (Eloquent driver) |
| Model | `App\Models\Customer` |
| Driver | Session (cookie-based) |
| Login route | `GET /account/login` |
| Registration route | `GET /account/register` |

#### Customer Login Flow

```mermaid
sequenceDiagram
    participant B as Browser
    participant LW as Livewire Component
    participant Auth as Auth Guard (customer)
    participant Store as ResolveStore Middleware
    participant DB as Database

    Note over Store: Store already resolved from hostname
    B->>LW: Submit email + password
    LW->>LW: Validate input
    LW->>LW: Check rate limiter (5/min per IP)
    LW->>Auth: attempt(credentials + store_id, remember)
    Auth->>DB: SELECT from customers WHERE email = ? AND store_id = ?
    Auth->>Auth: Verify bcrypt hash
    alt Valid
        Auth-->>LW: Success (regenerate session)
        LW-->>B: Redirect to /account (or intended URL)
    else Invalid
        Auth-->>LW: Failure
        LW-->>B: Flash "Invalid credentials"
    end
```

**Requirements:**

- Customer login is always scoped to the current store. The store is resolved from the request hostname by the `ResolveStore` middleware before the login attempt.
- The custom `CustomerUserProvider` must inject `store_id` into the credential check so the query filters by both `email` and `store_id`.
- The same email address can exist in different stores (multi-tenant isolation).
- Rate limiting: maximum 5 attempts per minute per IP, using the same `login` rate limiter.
- After login: redirect to `/account` or the intended URL (if the customer was redirected from a protected page).

#### Customer Registration Flow

```mermaid
flowchart TD
    A[Customer visits /account/register] --> B[Fill form: name, email, password, password_confirmation, marketing_opt_in]
    B --> C{Validate via RegisterCustomerRequest}
    C -- Invalid --> D[Show validation errors]
    C -- Valid --> E[Create Customer record scoped to current store]
    E --> F[Auto-login via customer guard]
    F --> G[Redirect to /account]
```

**Registration fields:**

| Field | Type | Required | Validation |
|-------|------|:--------:|------------|
| name | string | Y | max:255 |
| email | string | Y | valid email, unique per store (composite unique on email + store_id) |
| password | string | Y | min:8, confirmed |
| password_confirmation | string | Y | must match password |
| marketing_opt_in | boolean | N | defaults to false |

#### Customer Store Scoping

- The `customers` auth provider requires a custom `CustomerUserProvider` class.
- This provider overrides the default Eloquent provider to always include `store_id` in credential queries.
- The `store_id` is obtained from the container-bound current store (resolved by `…2904 chars truncated…orm pattern defined in Spec 03.

#### Dashboard

| Method | Path | Description | Component |
| GET | `/pages/{handle}` | Static content page (About, FAQ, etc.) | `Storefront\Pages\Show` |

#### Customer Account Routes

**Additional middleware:** `auth:customer`

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/account` | Customer account overview | `Storefront\Account\Dashboard` |
| GET | `/account/orders` | Order history list | `Storefront\Account\Orders\Index` |
| GET | `/account/orders/{orderNumber}` | Order detail | `Storefront\Account\Orders\Show` |
| GET | `/account/addresses` | Address book management | `Storefront\Account\Addresses\Index` |

#### Customer Auth Routes (no auth required)

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/account/login` | Customer login page | `Storefront\Account\Auth\Login` |
| GET | `/account/register` | Customer registration page | `Storefront\Account\Auth\Register` |
| GET | `/forgot-password` | Forgot password page | `Storefront\Account\Auth\ForgotPassword` |
| POST | `/forgot-password` | Send reset link | Handled by Livewire action |
| GET | `/reset-password/{token}` | Reset password form | `Storefront\Account\Auth\ResetPassword` |
| POST | `/reset-password` | Reset password | Handled by Livewire action |

---

### 1.4 Checkout Routes

**Middleware:** `web`, `store.resolve`

| Method | Path | Description | Component |
flowchart TD
    A[Home Page] --> B{Browse}
    B --> C[Collection Page]
    B --> D[Search Modal]
    D --> E[Search Results Page]
    C --> F[Product Page]
    E --> F
    F --> G{Select Variant}
    G --> H[Set Quantity]
    H --> I[Add to Cart]
    I --> J[Cart Drawer Opens]
    J --> K{Continue?}
    K -->|Continue Shopping| B
    K -->|Apply Discount| L[Enter Discount Code]
    L --> J
    K -->|Checkout| M[Checkout Page]
    M --> N[Step 1: Contact Info]
    N --> O[Step 2: Shipping Address]
    O --> P[Step 3: Shipping Method]
    P --> Q[Step 4: Payment Method & Pay]
    Q --> S[Order Confirmation]
    S --> T{Next?}
    T -->|Continue Shopping| A
    T -->|View Order| U[Account Order Detail]
```

### Account Management Flow

```mermaid
flowchart TD
    A[Login Page] --> B{Has Account?}
    B -->|No| C[Register Page]
    C --> D[Account Dashboard]
    B -->|Yes| D
    D --> E{Action}
    E --> F[Order History]
    F --> G[Order Detail]
    G --> H[Track Shipment]
    E --> I[Address Book]
    I --> J{Action}
    J --> K[Add Address]
    J --> L[Edit Address]
    J --> M[Delete Address]
    J --> N[Set Default Address]
    E --> O[Log Out]
```

### Cart Management Flow

```mermaid
flowchart TD
    A[Add to Cart] --> B[Cart Drawer Opens]
    B --> C{Action}
    C --> D[Update Quantity]
    D --> B
    C --> E[Remove Item]
    E --> F{Cart Empty?}
    F -->|Yes| G[Empty Cart State]
    G --> H[Continue Shopping]
    F -->|No| B
    C --> I[Apply Discount Code]
    I --> J{Valid?}
    J -->|Yes| K[Discount Applied]
    K --> B
    J -->|No| L[Error Message]
    L --> B
    C --> M[Proceed to Checkout]
    M --> N[Checkout Page]
    C --> O[View Full Cart Page]
    O --> N
```

### Component State Diagram

```mermaid
stateDiagram-v2
    [*] --> Empty: Cart initialized

    state "Cart Drawer" as CD {
        Empty --> HasItems: Add item
        HasItems --> HasItems: Update quantity
        HasItems --> HasItems: Apply discount
        HasItems --> Empty: Remove last item
        HasItems --> Checkout: Proceed to checkout
    }

    state "Product Page" as PP {
        NoVariant --> VariantSelected: Select options
        VariantSelected --> InStock: Stock > 0
        VariantSelected --> OutOfStock: Stock = 0
        InStock --> Adding: Click add to cart
        Adding --> InStock: Complete
        OutOfStock --> SoldOut: Deny policy
        OutOfStock --> Backorder: Continue policy
    }

    state "Checkout" as CO {
        Contact --> ShippingAddress: Complete step 1
        ShippingAddress --> ShippingMethod: Complete step 2
        ShippingMethod --> Payment: Complete step 3
        Payment --> Processing: Submit payment
        Processing --> Confirmation: Payment success
        Processing --> Payment: Payment failure
    }
```

---

## 21. Flash / Toast Messages

| Action | Message | Type |
|--------|---------|------|
| Item added to cart | "Added to cart" | success |
| Item removed from cart | "Item removed" | success |
| Cart is empty | "Your cart is empty" | info |
| Discount applied | "Discount applied" | success |
| Invalid discount code | "Invalid discount code" | error |
| Out of stock | "This product is currently out of stock" | warning |
| Registration success | "Account created successfully" | success |
| Login success | (redirect, no message) | - |
| Login failed | "Invalid credentials" | error |
| Password reset sent | "We have emailed your password reset link!" | success |
| Order placed | "Thank you for your order!" | success |


Output:
Chunk ID: 7dab79
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3226
Output:

- An optional "Remember me" checkbox on the login form.
- When enabled, Laravel sets a long-lived remember token cookie.
- The `remember_token` column on the `users` table stores the hashed token.
- Cookie name follows Laravel default convention: `remember_web_{hash}`.

#### Admin Password Reset Flow

```mermaid
sequenceDiagram
    participant B as Browser
    participant App as Application
    participant DB as Database
    participant Mail as Mail Service

    B->>App: GET /admin/forgot-password
    App-->>B: Render forgot-password form
    B->>App: POST /admin/forgot-password (email)
    App->>DB: Look up user by email
    alt User exists
        App->>DB: Store reset token in password_reset_tokens table
        App->>Mail: Send reset link email
    end
    App-->>B: "If that email exists, we sent a reset link." (generic)
    B->>App: GET /admin/reset-password/{token}
    App-->>B: Render reset-password form
    B->>App: POST /admin/reset-password (token, email, password, password_confirmation)
    App->>DB: Validate token (exists, not expired, matches email)
    alt Valid token
        App->>DB: Update user password (bcrypt hash)
        App->>DB: Delete used token
        App-->>B: Redirect to /admin/login with success flash
    else Invalid/expired token
        App-->>B: Error message
    end
```

**Requirements:**

- Uses Laravel's built-in password broker (`users` broker).
- Reset tokens stored in the `password_reset_tokens` table.
- Token expiration: 60 minutes (configurable in `config/auth.php` under `passwords.users.expire`).
- Throttle: one reset email per 60 seconds per email address.
- The "forgot password" response must be generic (do not reveal whether email exists).

#### Admin Logout

- Route: `POST /admin/logout` (via Livewire action or standard route).
- Steps: invalidate the entire session, regenerate the CSRF token, redirect to `/admin/login`.
- The response must not cache.

#### Session Lifetime

| Setting | Value |
|---------|-------|
| Lifetime | 120 minutes (configurable via `SESSION_LIFETIME` env var) |
| Expire on close | No (session persists until lifetime expires) |

---

### 1.2 Customer Authentication

| Setting | Value |
|---------|-------|
| Guard name | `customer` (custom guard) |
| Provider name | `customers` (Eloquent driver) |
| Model | `App\Models\Customer` |
| Driver | Session (cookie-based) |
| Login route | `GET /account/login` |
| Registration route | `GET /account/register` |

#### Customer Login Flow

```mermaid
sequenceDiagram
    participant B as Browser
    participant LW as Livewire Component
    participant Auth as Auth Guard (customer)
    participant Store as ResolveStore Middleware
    participant DB as Database

    Note over Store: Store already resolved from hostname
    B->>LW: Submit email + password
    LW->>LW: Validate input
    LW->>LW: Check rate limiter (5/min per IP)
    LW->>Auth: attempt(credentials + store_id, remember)
    Auth->>DB: SELECT from customers WHERE email = ? AND store_id = ?
    Auth->>Auth: Verify bcrypt hash
    alt Valid
        Auth-->>LW: Success (regenerate session)
        LW-->>B: Redirect to /account (or intended URL)
    else Invalid
        Auth-->>LW: Failure
        LW-->>B: Flash "Invalid credentials"
    end
```

**Requirements:**

- Customer login is always scoped to the current store. The store is resolved from the request hostname by the `ResolveStore` middleware before the login attempt.
- The custom `CustomerUserProvider` must inject `store_id` into the credential check so the query filters by both `email` and `store_id`.
- The same email address can exist in different stores (multi-tenant isolation).
- Rate limiting: maximum 5 attempts per minute per IP, using the same `login` rate limiter.
- After login: redirect to `/account` or the intended URL (if the customer was redirected from a protected page).

#### Customer Registration Flow

```mermaid
flowchart TD
    A[Customer visits /account/register] --> B[Fill form: name, email, password, password_confirmation, marketing_opt_in]
    B --> C{Validate via RegisterCustomerRequest}
    C -- Invalid --> D[Show validation errors]
    C -- Valid --> E[Create Customer record scoped to current store]
    E --> F[Auto-login via customer guard]
    F --> G[Redirect to /account]
```

**Registration fields:**

| Field | Type | Required | Validation |
|-------|------|:--------:|------------|
| name | string | Y | max:255 |
| email | string | Y | valid email, unique per store (composite unique on email + store_id) |
| password | string | Y | min:8, confirmed |
| password_confirmation | string | Y | must match password |
| marketing_opt_in | boolean | N | defaults to false |

#### Customer Store Scoping

- The `customers` auth provider requires a custom `CustomerUserProvider` class.
- This provider overrides the default Eloquent provider to always include `store_id` in credential queries.
- The `store_id` is obtained from the container-bound current store (resolved by `ResolveStore` middleware).
- This ensures that `Auth::guard('customer')->attempt()` never crosses store boundaries.

#### Customer Password Reset

- Uses a separate password broker: `customers` broker.
- Tokens stored in the `customer_password_reset_tokens` table (separate from admin tokens).
- Token records include `store_id` to scope resets to the correct store.
- Routes: `GET /forgot-password`, `POST /forgot-password`, `GET /reset-password/{token}`, `POST /reset-password`.
- Token expiration: 60 minutes.
- Throttle: one reset email per 60 seconds.
- Generic response on the "forgot password" form (do not reveal whether email exists in the store).

---

### 1.3 API Authentication (Sanctum)

| Setting | Value |
|---------|-------|
| Guard name | `sanctum` |
| Provider | `users` (admin users only) |
| Token type | Personal access tokens |
| Token prefix | `shop_` |
| Hashing | SHA-256 |
| Default expiration | 1 year |
| Storage | `personal_access_tokens` table |

#### Token Lifecycle

```mermaid
# API and Routing Specification

This document defines every route, endpoint, middleware stack, request/response schema, and rate limit for the e-commerce platform. The admin panel is built with Livewire v4 + Flux UI (server-rendered), while the storefront uses Blade + Livewire. REST API endpoints serve cart/checkout operations and programmatic admin access.

All monetary amounts are stored and transmitted in **minor units** (cents). All IDs are auto-incrementing integers. All monetary amounts are in minor units (cents).

---

## 1. Web Routes (Blade/Livewire Pages)

### 1.1 Admin Auth Routes

No middleware beyond the default `web` group. These routes handle admin login and logout.

| Method | Path | Description | Component / Action |
|--------|------|-------------|--------------------|
| GET | `/admin/login` | Admin login page | `Livewire: Admin\Auth\Login` |
| POST | `/admin/login` | Process admin login | Handled by Livewire action |
| POST | `/admin/logout` | Logout and invalidate session | Handled by Livewire action |
| GET | `/admin/forgot-password` | Show forgot password form | `Livewire: Admin\Auth\ForgotPassword` |
| POST | `/admin/forgot-password` | Send password reset link | Handled by Livewire action |
| GET | `/admin/reset-password/{token}` | Show reset password form | `Livewire: Admin\Auth\ResetPassword` |
| POST | `/admin/reset-password` | Reset password with token | Handled by Livewire action |

Rate limit: `login` tier (5 per minute per IP).

---

### 1.2 Admin Routes

**Prefix:** `/admin`
**Middleware:** `web`, `auth`, `verified`, `store.resolve`, `role.check`

All admin pages render Livewire full-page components. Data mutations happen via Livewire actions (not separate POST endpoints).

> **Note:** The Create and Edit routes render the same Livewire form component (`ProductForm`, `OrderForm`, etc.) in create vs edit mode, following the shared-form pattern defined in Spec 03.

#### Dashboard

| Method | Path | Description | Component |
| GET | `/pages/{handle}` | Static content page (About, FAQ, etc.) | `Storefront\Pages\Show` |

#### Customer Account Routes

**Additional middleware:** `auth:customer`

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/account` | Customer account overview | `Storefront\Account\Dashboard` |
| GET | `/account/orders` | Order history list | `Storefront\Account\Orders\Index` |
| GET | `/account/orders/{orderNumber}` | Order detail | `Storefront\Account\Orders\Show` |
| GET | `/account/addresses` | Address book management | `Storefront\Account\Addresses\Index` |

#### Customer Auth Routes (no auth required)

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/account/login` | Customer login page | `Storefront\Account\Auth\Login` |
| GET | `/account/register` | Customer registration page | `Storefront\Account\Auth\Register` |
| GET | `/forgot-password` | Forgot password page | `Storefront\Account\Auth\ForgotPassword` |
| POST | `/forgot-password` | Send reset link | Handled by Livewire action |
| GET | `/reset-password/{token}` | Reset password form | `Storefront\Account\Auth\ResetPassword` |
| POST | `/reset-password` | Reset password | Handled by Livewire action |

---

### 1.4 Checkout Routes

**Middleware:** `web`, `store.resolve`

| Method | Path | Description | Component |
flowchart TD
    A[Home Page] --> B{Browse}
    B --> C[Collection Page]
    B --> D[Search Modal]
    D --> E[Search Results Page]
    C --> F[Product Page]
    E --> F
    F --> G{Select Variant}
    G --> H[Set Quantity]
    H --> I[Add to Cart]
    I --> J[Cart Drawer Opens]
    J --> K{Continue?}
    K -->|Continue Shopping| B
    K -->|Apply Discount| L[Enter Discount Code]
    L --> J
    K -->|Checkout| M[Checkout Page]
    M --> N[Step 1: Contact Info]
    N --> O[Step 2: Shipping Address]
    O --> P[Step 3: Shipping Method]
    P --> Q[Step 4: Payment Method & Pay]
    Q --> S[Order Confirmation]
    S --> T{Next?}
    T -->|Continue Shopping| A
    T -->|View Order| U[Account Order Detail]
```

### Account Management Flow

```mermaid
flowchart TD
    A[Login Page] --> B{Has Account?}
    B -->|No| C[Register Page]
    C --> D[Account Dashboard]
    B -->|Yes| D
    D --> E{Action}
    E --> F[Order History]
    F --> G[Order Detail]
    G --> H[Track Shipment]
    E --> I[Address Book]
    I --> J{Action}
    J --> K[Add Address]
    J --> L[Edit Address]
    J --> M[Delete Address]
    J --> N[Set Default Address]
    E --> O[Log Out]
```

### Cart Management Flow

```mermaid
flowchart TD
    A[Add to Cart] --> B[Cart Drawer Opens]
    B --> C{Action}
    C --> D[Update Quantity]
    D --> B
    C --> E[Remove Item]
    E --> F{Cart Empty?}
    F -->|Yes| G[Empty Cart State]
    G --> H[Continue Shopping]
    F -->|No| B
    C --> I[Apply Discount Code]
    I --> J{Valid?}
    J -->|Yes| K[Discount Applied]
    K --> B
    J -->|No| L[Error Message]
    L --> B
    C --> M[Proceed to Checkout]
    M --> N[Checkout Page]
    C --> O[View Full Cart Page]
    O --> N
```

### Component State Diagram

```mermaid
stateDiagram-v2
    [*] --> Empty: Cart initialized

    state "Cart Drawer" as CD {
        Empty --> HasItems: Add item
        HasItems --> HasItems: Update quantity
        HasItems --> HasItems: Apply discount
        HasItems --> Empty: Remove last item
        HasItems --> Checkout: Proceed to checkout
    }

    state "Product Page" as PP {
        NoVariant --> VariantSelected: Select options
        VariantSelected --> InStock: Stock > 0
        VariantSelected --> OutOfStock: Stock = 0
        InStock --> Adding: Click add to cart
        Adding --> InStock: Complete
        OutOfStock --> SoldOut: Deny policy
        OutOfStock --> Backorder: Continue policy
    }

    state "Checkout" as CO {
        Contact --> ShippingAddress: Complete step 1
        ShippingAddress --> ShippingMethod: Complete step 2
        ShippingMethod --> Payment: Complete step 3
        Payment --> Processing: Submit payment
        Processing --> Confirmation: Payment success
        Processing --> Payment: Payment failure
    }
```

---

## 21. Flash / Toast Messages

| Action | Message | Type |
|--------|---------|------|
| Item added to cart | "Added to cart" | success |
| Item removed from cart | "Item removed" | success |
| Cart is empty | "Your cart is empty" | info |
| Discount applied | "Discount applied" | success |
| Invalid discount code | "Invalid discount code" | error |
| Out of stock | "This product is currently out of stock" | warning |
| Registration success | "Account created successfully" | success |
| Login success | (redirect, no message) | - |
| Login failed | "Invalid credentials" | error |
| Password reset sent | "We have emailed your password reset link!" | success |
| Order placed | "Thank you for your order!" | success |


php artisan route:list --path=login && php artisan route:list --path=register && php artisan route:list --name=login --except-vendor && php artisan route:list --name=login


Output:
Chunk ID: eab6bf
Wall time: 0.5904 seconds
Process exited with code 0
Original token count: 359
Output:

  GET|HEAD   account/login account.login › Livewire\Mechanisms › LivewirePage…
  GET|HEAD   admin/login admin.login › Livewire\Mechanisms › LivewirePageCont…
  GET|HEAD   login login › Laravel\Fortify › AuthenticatedSessionController@c…
  POST       login login.store › Laravel\Fortify › AuthenticatedSessionContro…

                                                            Showing [4] routes

  GET|HEAD   account/register account.register › Livewire\Mechanisms › Livewi…
  GET|HEAD   register register › Laravel\Fortify › RegisteredUserController@c…
  POST       register register.store › Laravel\Fortify › RegisteredUserContro…

                                                            Showing [3] routes

   ERROR  Your application doesn't have any routes matching the given criteria.

  GET|HEAD   account/login account.login › Livewire\Mechanisms › LivewirePage…
  GET|HEAD   admin/login admin.login › Livewire\Mechanisms › LivewirePageCont…
  GET|HEAD   login login › Laravel\Fortify › AuthenticatedSessionController@c…
  POST       login login.store › Laravel\Fortify › AuthenticatedSessionContro…
  GET|HEAD   two-factor-challenge two-factor.login › Laravel\Fortify › TwoFac…
  POST       two-factor-challenge two-factor.login.store › Laravel\Fortify › …

                                                            Showing [6] routes


php artisan route:list --path=login && php artisan route:list --path=register && php artisan route:list --name=login --except-vendor && php artisan route:list --name=login

  GET|HEAD   account/login account.login › Livewire\Mechanisms › LivewirePage…
  GET|HEAD   admin/login admin.login › Livewire\Mechanisms › LivewirePageCont…
  GET|HEAD   login login › Laravel\Fortify › AuthenticatedSessionController@c…
  POST       login login.store › Laravel\Fortify › AuthenticatedSessionContro…

                                                            Showing [4] routes

  GET|HEAD   account/register account.register › Livewire\Mechanisms › Livewi…
  GET|HEAD   register register › Laravel\Fortify › RegisteredUserController@c…
  POST       register register.store › Laravel\Fortify › RegisteredUserContro…

                                                            Showing [3] routes

   ERROR  Your application doesn't have any routes matching the given criteria.

  GET|HEAD   account/login account.login › Livewire\Mechanisms › LivewirePage…
  GET|HEAD   admin/login admin.login › Livewire\Mechanisms › LivewirePageCont…
  GET|HEAD   login login › Laravel\Fortify › AuthenticatedSessionController@c…
  POST       login login.store › Laravel\Fortify › AuthenticatedSessionContro…
  GET|HEAD   two-factor-challenge two-factor.login › Laravel\Fortify › TwoFac…
  POST       two-factor-challenge two-factor.login.store › Laravel\Fortify › …

                                                            Showing [6] routes


rg -n "function boot|loadRoutes|booted|registerRoutes|reset-password|forgot-password" vendor/laravel/fortify/src/FortifyServiceProvider.php vendor/laravel/fortify/routes/routes.php


rg -n "function boot|loadRoutes|booted|registerRoutes|reset-password|forgot-password" vendor/laravel/fortify/src/FortifyServiceProvider.php vendor/laravel/fortify/routes/routes.php

rg -n 'function boot|loadRoutes|booted|registerRoutes|reset-password|forgot-password' vendor/laravel/fortify/src/FortifyServiceProvider.php vendor/laravel/fortify/routes/routes.php

Question: function boot|loadRoutes|booted|registerRoutes|reset-password|forgot-password

vendor/laravel/fortify/routes/routes.php:51:            Route::get(RoutePath::for('password.request', '/forgot-password'), [PasswordResetLinkController::class, 'create'])
vendor/laravel/fortify/routes/routes.php:55:            Route::get(RoutePath::for('password.reset', '/reset-password/{token}'), [NewPasswordController::class, 'create'])
vendor/laravel/fortify/routes/routes.php:60:        Route::post(RoutePath::for('password.email', '/forgot-password'), [PasswordResetLinkController::class, 'store'])
vendor/laravel/fortify/routes/routes.php:64:        Route::post(RoutePath::for('password.update', '/reset-password'), [NewPasswordController::class, 'store'])
vendor/laravel/fortify/src/FortifyServiceProvider.php:118:    public function boot()
vendor/laravel/fortify/src/FortifyServiceProvider.php:167:                $this->loadRoutesFrom(__DIR__.'/../routes/routes.php');


Output:
Chunk ID: b7f061
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 220
Output:
vendor/laravel/fortify/routes/routes.php:51:            Route::get(RoutePath::for('password.request', '/forgot-password'), [PasswordResetLinkController::class, 'create'])
vendor/laravel/fortify/routes/routes.php:55:            Route::get(RoutePath::for('password.reset', '/reset-password/{token}'), [NewPasswordController::class, 'create'])
vendor/laravel/fortify/routes/routes.php:60:        Route::post(RoutePath::for('password.email', '/forgot-password'), [PasswordResetLinkController::class, 'store'])
vendor/laravel/fortify/routes/routes.php:64:        Route::post(RoutePath::for('password.update', '/reset-password'), [NewPasswordController::class, 'store'])
vendor/laravel/fortify/src/FortifyServiceProvider.php:118:    public function boot()
vendor/laravel/fortify/src/FortifyServiceProvider.php:167:                $this->loadRoutesFrom(__DIR__.'/../routes/routes.php');


sed -n '100,190p' vendor/laravel/fortify/src/FortifyServiceProvider.php && sed -n '40,75p' vendor/laravel/fortify/routes/routes.php && sed -n '1,120p' vendor/laravel/fortify/src/RoutePath.php


sed -n '100,190p' vendor/laravel/fortify/src/FortifyServiceProvider.php && sed -n '40,75p' vendor/laravel/fortify/routes/routes.php && sed -n '1,120p' vendor/laravel/fortify/src/RoutePath.php

RoutePath.php

        $this->app->singleton(PasswordUpdateResponseContract::class, PasswordUpdateResponse::class);
        $this->app->singleton(ProfileInformationUpdatedResponseContract::class, ProfileInformationUpdatedResponse::class);
        $this->app->singleton(RecoveryCodesGeneratedResponseContract::class, RecoveryCodesGeneratedResponse::class);
        $this->app->singleton(RegisterResponseContract::class, RegisterResponse::class);
        $this->app->singleton(EmailVerificationNotificationSentResponseContract::class, EmailVerificationNotificationSentResponse::class);
        $this->app->singleton(SuccessfulPasswordResetLinkRequestResponseContract::class, SuccessfulPasswordResetLinkRequestResponse::class);
        $this->app->singleton(TwoFactorConfirmedResponseContract::class, TwoFactorConfirmedResponse::class);
        $this->app->singleton(TwoFactorDisabledResponseContract::class, TwoFactorDisabledResponse::class);
        $this->app->singleton(TwoFactorEnabledResponseContract::class, TwoFactorEnabledResponse::class);
        $this->app->singleton(TwoFactorLoginResponseContract::class, TwoFactorLoginResponse::class);
        $this->app->singleton(VerifyEmailResponseContract::class, VerifyEmailResponse::class);
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        $this->configurePublishing();
        $this->configureRoutes();
        $this->registerCommands();
    }

    /**
     * Configure the publishable resources offered by the package.
     *
     * @return void
     */
    protected function configurePublishing()
    {
        if ($this->app->runningInConsole()) {
            $this->publishes([
                __DIR__.'/../stubs/fortify.php' => config_path('fortify.php'),
            ], 'fortify-config');

            $this->publishes([
                __DIR__.'/../stubs/CreateNewUser.php' => app_path('Actions/Fortify/CreateNewUser.php'),
                __DIR__.'/../stubs/FortifyServiceProvider.php' => app_path('Providers/FortifyServiceProvider.php'),
                __DIR__.'/../stubs/PasswordValidationRules.php' => app_path('Actions/Fortify/PasswordValidationRules.php'),
                __DIR__.'/../stubs/ResetUserPassword.php' => app_path('Actions/Fortify/ResetUserPassword.php'),
                __DIR__.'/../stubs/UpdateUserProfileInformation.php' => app_path('Actions/Fortify/UpdateUserProfileInformation.php'),
                __DIR__.'/../stubs/UpdateUserPassword.php' => app_path('Actions/Fortify/UpdateUserPassword.php'),
            ], 'fortify-support');

            $method = method_exists($this, 'publishesMigrations') ? 'publishesMigrations' : 'publishes';

            $this->{$method}([
                __DIR__.'/../database/migrations' => database_path('migrations'),
            ], 'fortify-migrations');
        }
    }

    /**
     * Configure the routes offered by the application.
     *
     * @return void
     */
    protected function configureRoutes()
    {
        if (Fortify::$registersRoutes) {
            Route::group([
                'namespace' => 'Laravel\Fortify\Http\Controllers',
                'domain' => config('fortify.domain', null),
                'prefix' => config('fortify.prefix'),
            ], function () {
                $this->loadRoutesFrom(__DIR__.'/../routes/routes.php');
            });
        }
    }

    /**
     * Register the package's commands.
     */
    protected function registerCommands(): void
    {
        if ($this->app->runningInConsole()) {
            $this->commands([
                Console\InstallCommand::class,
            ]);
        }
    }
}
            'guest:'.config('fortify.guard'),
            $limiter ? 'throttle:'.$limiter : null,
        ]))->name('login.store');

    Route::post(RoutePath::for('logout', '/logout'), [AuthenticatedSessionController::class, 'destroy'])
        ->middleware([config('fortify.auth_middleware', 'auth').':'.config('fortify.guard')])
        ->name('logout');

    // Password Reset...
    if (Features::enabled(Features::resetPasswords())) {
        if ($enableViews) {
            Route::get(RoutePath::for('password.request', '/forgot-password'), [PasswordResetLinkController::class, 'create'])
                ->middleware(['guest:'.config('fortify.guard')])
                ->name('password.request');

            Route::get(RoutePath::for('password.reset', '/reset-password/{token}'), [NewPasswordController::class, 'create'])
                ->middleware(['guest:'.config('fortify.guard')])
                ->name('password.reset');
        }

        Route::post(RoutePath::for('password.email', '/forgot-password'), [PasswordResetLinkController::class, 'store'])
            ->middleware(['guest:'.config('fortify.guard')])
            ->name('password.email');

        Route::post(RoutePath::for('password.update', '/reset-password'), [NewPasswordController::class, 'store'])
            ->middleware(['guest:'.config('fortify.guard')])
            ->name('password.update');
    }

    // Registration...
    if (Features::enabled(Features::registration())) {
        if ($enableViews) {
            Route::get(RoutePath::for('register', '/register'), [RegisteredUserController::class, 'create'])
                ->middleware(['guest:'.config('fortify.guard')])
                ->name('register');
        }
<?php

namespace Laravel\Fortify;

class RoutePath
{
    /**
     * Get the route path for the given route name.
     *
     * @param  string  $routeName
     * @param  string  $default
     * @return string
     */
    public static function for(string $routeName, string $default)
    {
        return config('fortify.paths.'.$routeName) ?? $default;
    }
}


Output:
Chunk ID: 28968d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1435
Output:
        $this->app->singleton(PasswordUpdateResponseContract::class, PasswordUpdateResponse::class);
        $this->app->singleton(ProfileInformationUpdatedResponseContract::class, ProfileInformationUpdatedResponse::class);
        $this->app->singleton(RecoveryCodesGeneratedResponseContract::class, RecoveryCodesGeneratedResponse::class);
        $this->app->singleton(RegisterResponseContract::class, RegisterResponse::class);
        $this->app->singleton(EmailVerificationNotificationSentResponseContract::class, EmailVerificationNotificationSentResponse::class);
        $this->app->singleton(SuccessfulPasswordResetLinkRequestResponseContract::class, SuccessfulPasswordResetLinkRequestResponse::class);
        $this->app->singleton(TwoFactorConfirmedResponseContract::class, TwoFactorConfirmedResponse::class);
        $this->app->singleton(TwoFactorDisabledResponseContract::class, TwoFactorDisabledResponse::class);
        $this->app->singleton(TwoFactorEnabledResponseContract::class, TwoFactorEnabledResponse::class);
        $this->app->singleton(TwoFactorLoginResponseContract::class, TwoFactorLoginResponse::class);
        $this->app->singleton(VerifyEmailResponseContract::class, VerifyEmailResponse::class);
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        $this->configurePublishing();
        $this->configureRoutes();
        $this->registerCommands();
    }

    /**
     * Configure the publishable resources offered by the package.
     *
     * @return void
     */
    protected function configurePublishing()
    {
        if ($this->app->runningInConsole()) {
            $this->publishes([
                __DIR__.'/../stubs/fortify.php' => config_path('fortify.php'),
            ], 'fortify-config');

            $this->publishes([
                __DIR__.'/../stubs/CreateNewUser.php' => app_path('Actions/Fortify/CreateNewUser.php'),
                __DIR__.'/../stubs/FortifyServiceProvider.php' => app_path('Providers/FortifyServiceProvider.php'),
                __DIR__.'/../stubs/PasswordValidationRules.php' => app_path('Actions/Fortify/PasswordValidationRules.php'),
                __DIR__.'/../stubs/ResetUserPassword.php' => app_path('Actions/Fortify/ResetUserPassword.php'),
                __DIR__.'/../stubs/UpdateUserProfileInformation.php' => app_path('Actions/Fortify/UpdateUserProfileInformation.php'),
                __DIR__.'/../stubs/UpdateUserPassword.php' => app_path('Actions/Fortify/UpdateUserPassword.php'),
            ], 'fortify-support');

            $method = method_exists($this, 'publishesMigrations') ? 'publishesMigrations' : 'publishes';

            $this->{$method}([
                __DIR__.'/../database/migrations' => database_path('migrations'),
            ], 'fortify-migrations');
        }
    }

    /**
     * Configure the routes offered by the application.
     *
     * @return void
     */
    protected function configureRoutes()
    {
        if (Fortify::$registersRoutes) {
            Route::group([
                'namespace' => 'Laravel\Fortify\Http\Controllers',
                'domain' => config('fortify.domain', null),
                'prefix' => config('fortify.prefix'),
            ], function () {
                $this->loadRoutesFrom(__DIR__.'/../routes/routes.php');
            });
        }
    }

    /**
     * Register the package's commands.
     */
    protected function registerCommands(): void
    {
        if ($this->app->runningInConsole()) {
            $this->commands([
                Console\InstallCommand::class,
            ]);
        }
    }
}
            'guest:'.config('fortify.guard'),
            $limiter ? 'throttle:'.$limiter : null,
        ]))->name('login.store');

    Route::post(RoutePath::for('logout', '/logout'), [AuthenticatedSessionController::class, 'destroy'])
        ->middleware([config('fortify.auth_middleware', 'auth').':'.config('fortify.guard')])
        ->name('logout');

    // Password Reset...
    if (Features::enabled(Features::resetPasswords())) {
        if ($enableViews) {
            Route::get(RoutePath::for('password.request', '/forgot-password'), [PasswordResetLinkController::class, 'create'])
                ->middleware(['guest:'.config('fortify.guard')])
                ->name('password.request');

            Route::get(RoutePath::for('password.reset', '/reset-password/{token}'), [NewPasswordController::class, 'create'])
                ->middleware(['guest:'.config('fortify.guard')])
                ->name('password.reset');
        }

        Route::post(RoutePath::for('password.email', '/forgot-password'), [PasswordResetLinkController::class, 'store'])
            ->middleware(['guest:'.config('fortify.guard')])
            ->name('password.email');

        Route::post(RoutePath::for('password.update', '/reset-password'), [NewPasswordController::class, 'store'])
            ->middleware(['guest:'.config('fortify.guard')])
            ->name('password.update');
    }

    // Registration...
    if (Features::enabled(Features::registration())) {
        if ($enableViews) {
            Route::get(RoutePath::for('register', '/register'), [RegisteredUserController::class, 'create'])
                ->middleware(['guest:'.config('fortify.guard')])
                ->name('register');
        }
<?php

namespace Laravel\Fortify;

class RoutePath
{
    /**
     * Get the route path for the given route name.
     *
     * @param  string  $routeName
     * @param  string  $default
     * @return string
     */
    public static function for(string $routeName, string $default)
    {
        return config('fortify.paths.'.$routeName) ?? $default;
    }
}


I’ve confirmed the spec names the customer reset pages at root paths, while the current implementation only exposes them under `/account/*` to avoid Fortify’s starter routes. The clean way to close this is to move Fortify’s generic reset URLs away from those root paths, then make the customer Livewire reset pages available at both the spec paths and the existing account aliases.


sed -n '1,220p' /Users/fabianwesner/Herd/shop/.agents/skills/laravel-best-practices/SKILL.md


sed -n '1,220p' /Users/fabianwesner/Herd/shop/.agents/skills/pest-testing/SKILL.md


sed -n '1,180p' /Users/fabianwesner/Herd/shop/.agents/skills/livewire-development/SKILL.md


sed -n '1,180p' /Users/fabianwesner/Herd/shop/.agents/skills/developing-with-fortify/SKILL.md


sed -n '1,180p' /Users/fabianwesner/Herd/shop/.agents/skills/developing-with-fortify/SKILL.md

---
name: developing-with-fortify
description: Laravel Fortify headless authentication backend development. Activate when implementing authentication features including login, registration, password reset, email verification, two-factor authentication (2FA/TOTP), profile updates, headless auth, authentication scaffolding, or auth guards in Laravel applications.
---

# Laravel Fortify Development

Fortify is a headless authentication backend that provides authentication routes and controllers for Laravel applications.

## Documentation

Use `search-docs` for detailed Laravel Fortify patterns and documentation.

## Usage

- **Routes**: Use `list-routes` with `only_vendor: true` and `action: "Fortify"` to see all registered endpoints
- **Actions**: Check `app/Actions/Fortify/` for customizable business logic (user creation, password validation, etc.)
- **Config**: See `config/fortify.php` for all options including features, guards, rate limiters, and username field
- **Contracts**: Look in `Laravel\Fortify\Contracts\` for overridable response classes (`LoginResponse`, `LogoutResponse`, etc.)
- **Views**: All view callbacks are set in `FortifyServiceProvider::boot()` using `Fortify::loginView()`, `Fortify::registerView()`, etc.

## Available Features

Enable in `config/fortify.php` features array:

- `Features::registration()` - User registration
- `Features::resetPasswords()` - Password reset via email
- `Features::emailVerification()` - Requires User to implement `MustVerifyEmail`
- `Features::updateProfileInformation()` - Profile updates
- `Features::updatePasswords()` - Password changes
- `Features::twoFactorAuthentication()` - 2FA with QR codes and recovery codes

> Use `search-docs` for feature configuration options and customization patterns.

## Setup Workflows

### Two-Factor Authentication Setup

```
- [ ] Add TwoFactorAuthenticatable trait to User model
- [ ] Enable feature in config/fortify.php
- [ ] Run migrations for 2FA columns
- [ ] Set up view callbacks in FortifyServiceProvider
- [ ] Create 2FA management UI
- [ ] Test QR code and recovery codes
```

> Use `search-docs` for TOTP implementation and recovery code handling patterns.

### Email Verification Setup

```
- [ ] Enable emailVerification feature in config
- [ ] Implement MustVerifyEmail interface on User model
- [ ] Set up verifyEmailView callback
- [ ] Add verified middleware to protected routes
- [ ] Test verification email flow
```

> Use `search-docs` for MustVerifyEmail implementation patterns.

### Password Reset Setup

```
- [ ] Enable resetPasswords feature in config
- [ ] Set up requestPasswordResetLinkView callback
- [ ] Set up resetPasswordView callback
- [ ] Define password.reset named route (if views disabled)
- [ ] Test reset email and link flow
```

> Use `search-docs` for custom password reset flow patterns.

### SPA Authentication Setup

```
- [ ] Set 'views' => false in config/fortify.php
- [ ] Install and configure Laravel Sanctum
- [ ] Use 'web' guard in fortify config
- [ ] Set up CSRF token handling
- [ ] Test XHR authentication flows
```

> Use `search-docs` for integration and SPA authentication patterns.

## Best Practices

### Custom Authentication Logic

Override authentication behavior using `Fortify::authenticateUsing()` for custom user retrieval or `Fortify::authenticateThrough()` to customize the authentication pipeline. Override response contracts in `AppServiceProvider` for custom redirects.

### Registration Customization

Modify `app/Actions/Fortify/CreateNewUser.php` to customize user creation logic, validation rules, and additional fields.

### Rate Limiting

Configure via `fortify.limiters.login` in config. Default configuration throttles by username + IP combination.

## Key Endpoints

| Feature                | Method   | Endpoint                                    |
|------------------------|----------|---------------------------------------------|
| Login                  | POST     | `/login`                                    |
| Logout                 | POST     | `/logout`                                   |
| Register               | POST     | `/register`                                 |
| Password Reset Request | POST     | `/forgot-password`                          |
| Password Reset         | POST     | `/reset-password`                           |
| Email Verify Notice    | GET      | `/email/verify`                             |
| Resend Verification    | POST     | `/email/verification-notification`          |
| Password Confirm       | POST     | `/user/confirm-password`                    |
| Enable 2FA             | POST     | `/user/two-factor-authentication`           |
| Confirm 2FA            | POST     | `/user/confirmed-two-factor-authentication` |
| 2FA Challenge          | POST     | `/two-factor-challenge`                     |
| Get QR Code            | GET      | `/user/two-factor-qr-code`                  |
| Recovery Codes         | GET/POST | `/user/two-factor-recovery-codes`           |


sed -n '1,180p' /Users/fabianwesner/Herd/shop/.agents/skills/livewire-development/SKILL.md

---
name: livewire-development
description: "Use for any task or question involving Livewire. Activate if user mentions Livewire, wire: directives, or Livewire-specific concepts like wire:model, wire:click, wire:sort, or islands, invoke this skill. Covers building new components, debugging reactivity issues, real-time form validation, drag-and-drop, loading states, migrating from Livewire 3 to 4, converting component formats (SFC/MFC/class-based), and performance optimization. Do not use for non-Livewire reactive UI (React, Vue, Alpine-only, Inertia.js) or standard Laravel forms without Livewire."
license: MIT
metadata:
  author: laravel
---

# Livewire Development

## Documentation

Use `search-docs` for detailed Livewire 4 patterns and documentation.

## Basic Usage

### Creating Components

```bash

# Single-file component (SFC - default in v4)

# Creates: resources/views/components/⚡create-post.blade.php

php artisan make:livewire create-post

# Page component (SFC - Full Page in v4)

# Creates: resources/views/pages/⚡create-post.blade.php

php artisan make:livewire pages::create-post

# Multi-file component (MFC)

# Creates: resources/views/components/⚡create-post/create-post.php

#          resources/views/components/⚡create-post/create-post.blade.php

php artisan make:livewire create-post --mfc

# Class-based component (v3 style)

# Creates: app/Livewire/CreatePost.php AND resources/views/livewire/create-post.blade.php

php artisan make:livewire create-post --class

# With namespace

php artisan make:livewire Posts/CreatePost
```

### Converting Between Formats

Use `php artisan livewire:convert create-post` to convert between single-file, multi-file, and class-based formats.

### Choosing a Component Format

> **Always follow the project's existing conventions first.** Before creating any component, inspect the project's existing Livewire components to determine the established format (SFC, MFC, or class-based) and directory structure. Check `app/Livewire/`, `resources/views/components/`, and `resources/views/livewire/` for existing components. If the project already uses a consistent format, **use that same format** — even if it differs from the Livewire v4 defaults below. Only fall back to the v4 defaults (SFC in `resources/views/components/`) when no existing convention is established.

Also check `config/livewire.php` for `make_command.type`, `make_command.emoji`, `component_locations`, and `component_namespaces` overrides, which change the default format and where files are stored.

### Component Format Reference

| Format | Flag | Class Path | View Path |
|--------|------|------------|-----------|
| Single-file (SFC) | default | — | `resources/views/components/⚡create-post.blade.php` (PHP + Blade in one file) |
| Full Page SFC | `pages::name` | — | `resources/views/pages/⚡create-post.blade.php` |
| Multi-file (MFC) | `--mfc` | `resources/views/components/⚡create-post/create-post.php` | `resources/views/components/⚡create-post/create-post.blade.php` |
| Class-based | `--class` | `app/Livewire/CreatePost.php` | `resources/views/livewire/create-post.blade.php` |
| View-based | default (Blade-only) | — | `resources/views/components/⚡create-post.blade.php` (Blade-only with functional state) |

> **Important:** The ⚡ prefix shown above is the **default** behavior in Livewire v4 — it is **configurable**. Check `config/livewire.php` for the `make_command.emoji` setting. When `true` (default), always include the ⚡ prefix in filenames you create. When `false`, omit the ⚡ prefix from all paths above.

Namespaced components map to subdirectories: `make:livewire Posts/CreatePost` creates `resources/views/components/posts/⚡create-post.blade.php` (single-file by default). Use `make:livewire Posts/CreatePost --mfc` for multi-file output at `resources/views/components/posts/⚡create-post/create-post.php` and `resources/views/components/posts/⚡create-post/create-post.blade.php`.

### Single-File Component Example

<!-- Single-File Component Example -->
```php
<?php
use Livewire\Component;

new class extends Component {
    public int $count = 0;

    public function increment(): void
    {
        $this->count++;
    }
};
?>

<div>
    <button wire:click="increment">Count: @{{ $count }}</button>
</div>
```

## Livewire 4 Specifics

### Key Changes From Livewire 3

These things changed in Livewire 4, but may not have been updated in this application. Verify this application's setup to ensure you follow existing conventions.

- Use `Route::livewire()` for full-page components (e.g., `Route::livewire('/posts/create', CreatePost::class)`); config keys renamed: `layout` → `component_layout`, `lazy_placeholder` → `component_placeholder`.
- `wire:model` now ignores child events by default (use `wire:model.deep` for old behavior); `wire:scroll` renamed to `wire:navigate:scroll`.
- Component tags must be properly closed; `wire:transition` now uses View Transitions API (modifiers removed).
- JavaScript: `$wire.$js('name', fn)` → `$wire.$js.name = fn`; `commit`/`request` hooks → `interceptMessage()`/`interceptRequest()`.

### New Features

- Component formats: single-file (SFC), multi-file (MFC), view-based components.
- Islands (`@island`) for isolated updates; async actions (`wire:click.async`, `#[Async]`) for parallel execution.
- Deferred/bundled loading: `defer`, `lazy.bundle` for optimized component loading.

| Feature | Usage | Purpose |
|---------|-------|---------|
| Islands | `@island(name: 'stats')` | Isolated update regions |
| Async | `wire:click.async` or `#[Async]` | Non-blocking actions |
| Deferred | `defer` attribute | Load after page render |
| Bundled | `lazy.bundle` | Load multiple together |

### New Directives

- `wire:sort`, `wire:intersect`, `wire:ref`, `.renderless`, `.preserve-scroll` are available for use.
- `data-loading` attribute automatically added to elements triggering network requests.

| Directive | Purpose |
|-----------|---------|
| `wire:sort` | Drag-and-drop sorting |
| `wire:intersect` | Viewport intersection detection |
| `wire:ref` | Element references for JS |
| `.renderless` | Component without rendering |
| `.preserve-scroll` | Preserve scroll position |

## Best Practices

- Always use `wire:key` in loops
- Use `wire:loading` for loading states
- Use `wire:model.live` for instant updates (default is debounced)
- Validate and authorize in actions (treat like HTTP requests)

## Configuration

- `smart_wire_keys` defaults to `true`; new configs: `component_locations`, `component_namespaces`, `make_command`, `csp_safe`.

## Alpine & JavaScript

- `wire:transition` uses browser View Transitions API; `$errors` and `$intercept` magic properties available.
- Non-blocking `wire:poll` and parallel `wire:model.live` updates improve performance.

For interceptors and hooks, see [reference/javascript-hooks.md](reference/javascript-hooks.md).

## Testing

<!-- Testing Example -->
```php
Livewire::test(Counter::class)
    ->assertSet('count', 0)
    ->call('increment')
    ->assertSet('count', 1);
```

## Verification

1. Browser console: Check for JS errors
2. Network tab: Verify Livewire requests return 200
3. Ensure `wire:key` on all `@foreach` loops

## Common Pitfalls

- Missing `wire:key` in loops → unexpected re-rendering
- Expecting `wire:model` real-time → use `wire:model.live`
- Unclosed component tags → syntax errors in v4
- Using deprecated config keys or JS hooks
- Including Alpine.js separately (already bundled in Livewire 4)