diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 85abc69..28fde1a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: true matrix: - stack: [blade, react, vue, svelte, api] + stack: [blade, react, vue, svelte, api, livewire, livewire-functional] laravel: [11] args: ["", --pest, --module=Authentication] include: diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index da34f5f..f1c25e6 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -28,7 +28,7 @@ #[AsCommand(name: 'flextra:install')] final class InstallCommand extends Command implements PromptsForMissingInput { - use InstallModuleApi, InstallModuleBlade, InstallReactWithInertia, InstallSvelteWithInertia, InstallVueWithInertia; + use InstallModuleApi, InstallModuleBlade, InstallModuleLivewire, InstallReactWithInertia, InstallSvelteWithInertia, InstallVueWithInertia; /** * The console command signature and name. @@ -79,11 +79,19 @@ public function handle(): ?int return $this->installBladeModule($this->moduleName); } + if ($this->argument('stack') === 'livewire') { + return $this->installLivewireModule($this->moduleName, false); + } + + if ($this->argument('stack') === 'livewire-functional') { + return $this->installLivewireModule($this->moduleName, true); + } + if ($this->argument('stack') === 'api') { return $this->installApiModule($this->moduleName); } - $this->components->error('Invalid stack. Supported stacks are [react], [vue], [svelte] and [blade].'); + $this->components->error('Invalid stack. Supported stacks are [react], [vue], [svelte], [livewire], [livewire-functional] and [blade].'); return 1; } @@ -162,6 +170,8 @@ protected function promptForMissingArgumentsUsing(): array 'react' => 'Inertia React with Laravel Modules', 'vue' => 'Inertia Vue with Laravel Modules', 'svelte' => 'Inertia Svelte with Laravel Modules', + 'livewire' => 'Livewire (Volt Class API) with Alpine and Laravel Modules', + 'livewire-functional' => 'Livewire (Functional API) with Alpine and Laravel Modules', 'api' => 'API with Laravel Modules', ], scroll: 6, @@ -196,7 +206,7 @@ protected function afterPromptingForMissingArguments(InputInterface $input, Outp ], hint: 'Use the space bar to select options.' ))->each(fn ($option) => $input->setOption($option, true)); - } elseif ($stack === 'blade') { + } elseif (in_array($stack, ['blade', 'livewire', 'livewire-functional'])) { $input->setOption('dark', confirm( label: 'Would you like dark mode support?', default: false @@ -212,6 +222,7 @@ protected function afterPromptingForMissingArguments(InputInterface $input, Outp /** * install module-inertia-react tests from stub + * * @throws JsonException */ protected function installTests(): bool @@ -219,6 +230,8 @@ protected function installTests(): bool (new Filesystem)->ensureDirectoryExists(base_path('tests/Feature')); $stubStack = match ($this->argument('stack')) { + 'livewire' => 'livewire-common', + 'livewire-functional' => 'livewire-common', 'api' => 'api-module', 'blade' => 'blade-module', default => 'inertia-php', @@ -490,6 +503,7 @@ private function removeDarkClasses(Finder $finder): void /** * Install Module Dependencies + * * @throws JsonException */ private function installModuleDependencies(bool $api = false): void diff --git a/src/Console/InstallModuleApi.php b/src/Console/InstallModuleApi.php index 2b34739..9f0e7af 100644 --- a/src/Console/InstallModuleApi.php +++ b/src/Console/InstallModuleApi.php @@ -1,109 +1,106 @@ -runCommands(['php artisan install:api']); - $this->installModuleDependencies(true); - - $files = new Filesystem; - - // Controllers... - $this->copyModuleFilesWithNamespace( - $moduleName, - __DIR__.'/../../stubs/api-module/app/Http/Controllers/Auth', - base_path('Modules/'.$moduleName.'/app/Http/Controllers') - ); - - // Middleware... - $files->copyDirectory(__DIR__.'/../../stubs/api-module/app/Http/Middleware', app_path('Http/Middleware')); - - $this->installMiddlewareAliases([ - 'verified' => '\App\Http\Middleware\EnsureEmailIsVerified::class', - ]); - - $this->installMiddleware([ - '\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class', - ], 'api', 'prepend'); - - // Requests... - $this->copyModuleFilesWithNamespace( - $moduleName, - __DIR__.'/../../stubs/api-module/app/Http/Requests/Auth', - base_path('Modules/'.$moduleName.'/app/Http/Requests') - ); - - // Providers... - $files->copyDirectory(__DIR__.'/../../stubs/api-module/app/Providers', app_path('Providers')); - - // Routes... - $this->copyFileWithNamespace($moduleName, __DIR__.'/../../stubs/api-module/routes/web.php', base_path('Modules/'.$moduleName.'/routes/web.php')); - $this->copyFileWithNamespace($moduleName, __DIR__.'/../../stubs/api-module/routes/auth.php', base_path('Modules/'.$moduleName.'/routes/auth.php')); - $this->copyFileWithNamespace($moduleName, __DIR__.'/../../stubs/api-module/routes/api.php', base_path('Modules/'.$moduleName.'/routes/api.php')); - - // Configuration... - $files->copyDirectory(__DIR__.'/../../stubs/api-module/config', config_path()); - - // Environment... - if (! $files->exists(base_path('.env'))) { - copy(base_path('.env.example'), base_path('.env')); - } - - file_put_contents( - base_path('.env'), - preg_replace('/APP_URL=(.*)/', 'APP_URL=http://localhost:8000'.PHP_EOL.'FRONTEND_URL=http://localhost:3000', file_get_contents(base_path('.env'))) - ); - - // Tests... - if (! $this->installTests()) { - return 1; - } - - $files->delete(base_path('tests/Feature/Auth/PasswordConfirmationTest.php')); - - // Cleaning... - $this->removeScaffoldingUnnecessaryForApis(); - - $this->components->info('Flextra Api for '.$moduleName.' Module scaffolding installed successfully.'); - - return 0; - } - - /** - * Remove any application scaffolding that isn't needed for APIs. - * - * @return void - */ - protected function removeScaffoldingUnnecessaryForApis(): void - { - $files = new Filesystem; - - // Remove frontend related files... - $files->delete(base_path('package.json')); - $files->delete(base_path('vite.config.js')); - $files->delete(base_path('tailwind.config.js')); - $files->delete(base_path('postcss.config.js')); - - // Remove Laravel "welcome" view... - $files->delete(resource_path('views/welcome.blade.php')); - $files->put(resource_path('views/.gitkeep'), PHP_EOL); - - // Remove CSS and JavaScript directories... - $files->deleteDirectory(resource_path('css')); - $files->deleteDirectory(resource_path('js')); - } - -} +runCommands(['php artisan install:api']); + $this->installModuleDependencies(true); + + $files = new Filesystem; + + // Controllers... + $this->copyModuleFilesWithNamespace( + $moduleName, + __DIR__.'/../../stubs/api-module/app/Http/Controllers/Auth', + base_path('Modules/'.$moduleName.'/app/Http/Controllers') + ); + + // Middleware... + $files->copyDirectory(__DIR__.'/../../stubs/api-module/app/Http/Middleware', app_path('Http/Middleware')); + + $this->installMiddlewareAliases([ + 'verified' => '\App\Http\Middleware\EnsureEmailIsVerified::class', + ]); + + $this->installMiddleware([ + '\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class', + ], 'api', 'prepend'); + + // Requests... + $this->copyModuleFilesWithNamespace( + $moduleName, + __DIR__.'/../../stubs/api-module/app/Http/Requests/Auth', + base_path('Modules/'.$moduleName.'/app/Http/Requests') + ); + + // Providers... + $files->copyDirectory(__DIR__.'/../../stubs/api-module/app/Providers', app_path('Providers')); + + // Routes... + $this->copyFileWithNamespace($moduleName, __DIR__.'/../../stubs/api-module/routes/web.php', base_path('Modules/'.$moduleName.'/routes/web.php')); + $this->copyFileWithNamespace($moduleName, __DIR__.'/../../stubs/api-module/routes/auth.php', base_path('Modules/'.$moduleName.'/routes/auth.php')); + $this->copyFileWithNamespace($moduleName, __DIR__.'/../../stubs/api-module/routes/api.php', base_path('Modules/'.$moduleName.'/routes/api.php')); + + // Configuration... + $files->copyDirectory(__DIR__.'/../../stubs/api-module/config', config_path()); + + // Environment... + if (! $files->exists(base_path('.env'))) { + copy(base_path('.env.example'), base_path('.env')); + } + + file_put_contents( + base_path('.env'), + preg_replace('/APP_URL=(.*)/', 'APP_URL=http://localhost:8000'.PHP_EOL.'FRONTEND_URL=http://localhost:3000', file_get_contents(base_path('.env'))) + ); + + // Tests... + if (! $this->installTests()) { + return 1; + } + + $files->delete(base_path('tests/Feature/Auth/PasswordConfirmationTest.php')); + + // Cleaning... + $this->removeScaffoldingUnnecessaryForApis(); + + $this->components->info('Flextra Api for '.$moduleName.' Module scaffolding installed successfully.'); + + return 0; + } + + /** + * Remove any application scaffolding that isn't needed for APIs. + */ + protected function removeScaffoldingUnnecessaryForApis(): void + { + $files = new Filesystem; + + // Remove frontend related files... + $files->delete(base_path('package.json')); + $files->delete(base_path('vite.config.js')); + $files->delete(base_path('tailwind.config.js')); + $files->delete(base_path('postcss.config.js')); + + // Remove Laravel "welcome" view... + $files->delete(resource_path('views/welcome.blade.php')); + $files->put(resource_path('views/.gitkeep'), PHP_EOL); + + // Remove CSS and JavaScript directories... + $files->deleteDirectory(resource_path('css')); + $files->deleteDirectory(resource_path('js')); + } +} diff --git a/src/Console/InstallModuleLivewire.php b/src/Console/InstallModuleLivewire.php new file mode 100644 index 0000000..1a2c06b --- /dev/null +++ b/src/Console/InstallModuleLivewire.php @@ -0,0 +1,194 @@ +installModuleDependencies(); + // NPM Packages... + $this->updateNodePackages(function ($packages) { + return [ + '@tailwindcss/forms' => '^0.5.2', + 'autoprefixer' => '^10.4.2', + 'postcss' => '^8.4.31', + 'tailwindcss' => '^3.1.0', + ] + $packages; + }); + + // Install Livewire... + if (! $this->requireComposerPackages(['livewire/livewire:^3.4', 'livewire/volt:^1.0'])) { + return 1; + } + + + // Install Volt... + (new Process([$this->phpBinary(), 'artisan', 'volt:install'], base_path())) + ->setTimeout(null) + ->run(); + + // Install Livewire Modules... + $this->installModuleLivewireDependencies(); + + // Controllers + $this->copyFileWithNamespace( + $moduleName, + __DIR__.'/../../stubs/blade-module/app/Http/Controllers/Auth/VerifyEmailController.php', + base_path('Modules/'.$moduleName.'/app/Http/Controllers/VerifyEmailController.php') + ); + + // Views Livewire common ... + $this->copyModuleFilesWithNamespace( + $moduleName, + __DIR__.'/../../stubs/livewire-common/resources/views/layouts', + base_path('Modules/'.$moduleName.'/resources/views/layouts') + ); + $this->copyModuleFilesWithNamespace( + $moduleName, + __DIR__.'/../../stubs/livewire-common/resources/views/components', + base_path('Modules/'.$moduleName.'/resources/views/components') + ); + $this->copyFileWithNamespace( + $moduleName, + __DIR__.'/../../stubs/livewire-common/resources/views/dashboard.blade.php', + base_path('Modules/'.$moduleName.'/resources/views/dashboard.blade.php') + ); + $this->copyFileWithNamespace( + $moduleName, + __DIR__.'/../../stubs/livewire-common/resources/views/profile.blade.php', + base_path('Modules/'.$moduleName.'/resources/views/profile.blade.php') + ); + $this->copyFileWithNamespace( + $moduleName, + __DIR__.'/../../stubs/livewire-common/resources/views/welcome.blade.php', + base_path('Modules/'.$moduleName.'/resources/views/welcome.blade.php') + ); + + // Livewire Components... + $this->copyModuleFilesWithNamespace( + $moduleName, + __DIR__.'/../../stubs/'.($functional ? 'livewire-functional' : 'livewire') + .'/resources/views/livewire/layout', + base_path('Modules/'.$moduleName.'/resources/views/livewire/layout') + ); + (new Filesystem)->ensureDirectoryExists(base_path('Modules/'.$moduleName.'/resources/views/livewire/pages')); + $this->copyModuleFilesWithNamespace( + $moduleName, + __DIR__.'/../../stubs/'.($functional ? 'livewire-functional' : 'livewire') + .'/resources/views/livewire/pages/auth', + base_path('Modules/'.$moduleName.'/resources/views/livewire/pages/auth') + ); + $this->copyModuleFilesWithNamespace( + $moduleName, + __DIR__.'/../../stubs/'.($functional ? 'livewire-functional' : 'livewire') + .'/resources/views/livewire/profile', + base_path('Modules/'.$moduleName.'/resources/views/livewire/profile') + ); + $this->copyModuleFilesWithNamespace( + $moduleName, + __DIR__.'/../../stubs/'.($functional ? 'livewire-functional' : 'livewire') + .'/resources/views/livewire/welcome', + base_path('Modules/'.$moduleName.'/resources/views/livewire/welcome') + ); + + // Views Components... + $this->copyModuleFilesWithNamespace( + $moduleName, + __DIR__.'/../../stubs/blade-module/resources/views/components', + base_path('Modules/'.$moduleName.'/resources/views/components') + ); + + // Components... + $this->copyModuleFilesWithNamespace( + $moduleName, + __DIR__.'/../../stubs/blade-module/app/View/Components', + base_path('Modules/'.$moduleName.'/app/View/Components') + ); + + // Actions... + $this->copyModuleFilesWithNamespace( + $moduleName, + __DIR__.'/../../stubs/livewire-common/app/Livewire/Actions', + base_path('Modules/'.$moduleName.'/app/Livewire/Actions') + ); + + // Forms... + $this->copyModuleFilesWithNamespace( + $moduleName, + __DIR__.'/../../stubs/livewire-common/app/Livewire/Forms', + base_path('Modules/'.$moduleName.'/app/Livewire/Forms') + ); + + // Dark mode... + if (! $this->option('dark')) { + $this->removeDarkClasses((new Finder) + ->in(base_path('Modules/'.$moduleName.'/resources/views')) + ->name('*.blade.php') + ->notPath('livewire/welcome/navigation.blade.php') + ->notName('welcome.blade.php') + ); + } + + // Tests... + if (! $this->installTests()) { + return 1; + } + + // Routes... + $this->copyModuleFilesWithNamespace( + $moduleName, + __DIR__.'/../../stubs/livewire-common/routes', + base_path('Modules/'.$moduleName.'/routes') + ); + // Tailwind / Vite... + copy(__DIR__.'/../../stubs/blade-module/tailwind.config.js', base_path('tailwind.config.js')); + copy(__DIR__.'/../../stubs/blade-module/postcss.config.js', base_path('postcss.config.js')); + copy(__DIR__.'/../../stubs/blade-module/vite.config.js', base_path('vite.config.js')); + copy(__DIR__.'/../../stubs/blade-module/resources/css/app.css', resource_path('css/app.css')); + + $this->components->info('Installing and building Node dependencies.'); + + if (file_exists(base_path('pnpm-lock.yaml'))) { + $this->runCommands(['pnpm install', 'pnpm run build']); + } elseif (file_exists(base_path('yarn.lock'))) { + $this->runCommands(['yarn install', 'yarn run build']); + } elseif (file_exists(base_path('bun.lock')) || file_exists(base_path('bun.lockb'))) { + $this->runCommands(['bun install', 'bun run build']); + } elseif (file_exists(base_path('deno.lock'))) { + $this->runCommands(['deno install', 'deno task build']); + } else { + $this->runCommands(['npm install', 'npm run build']); + } + + $this->components->info('Flextra Modules '.$moduleName.' Livewire scaffolding installed successfully.'); + + return 0; + } + + /** + * install laravel module livewire dependencies + * + */ + protected function installModuleLivewireDependencies(): void + { + // Check if laravel Modules installed and install it + if (! InstalledVersions::isInstalled('mhmiton/laravel-modules-livewire')) { + $this->runCommands(['composer require mhmiton/laravel-modules-livewire --dev']); + $this->runCommands(['php artisan vendor:publish --tag=modules-livewire-config']); + $this->runCommands(['composer dump-autoload']); + } + + } +} diff --git a/stubs/livewire-common/app/Livewire/Actions/Logout.php b/stubs/livewire-common/app/Livewire/Actions/Logout.php new file mode 100644 index 0000000..2db5c6c --- /dev/null +++ b/stubs/livewire-common/app/Livewire/Actions/Logout.php @@ -0,0 +1,20 @@ +logout(); + + Session::invalidate(); + Session::regenerateToken(); + } +} diff --git a/stubs/livewire-common/app/Livewire/Forms/LoginForm.php b/stubs/livewire-common/app/Livewire/Forms/LoginForm.php new file mode 100644 index 0000000..5c145bf --- /dev/null +++ b/stubs/livewire-common/app/Livewire/Forms/LoginForm.php @@ -0,0 +1,72 @@ +ensureIsNotRateLimited(); + + if (! Auth::attempt($this->only(['email', 'password']), $this->remember)) { + RateLimiter::hit($this->throttleKey()); + + throw ValidationException::withMessages([ + 'form.email' => trans('auth.failed'), + ]); + } + + RateLimiter::clear($this->throttleKey()); + } + + /** + * Ensure the authentication request is not rate limited. + */ + protected function ensureIsNotRateLimited(): void + { + if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { + return; + } + + event(new Lockout(request())); + + $seconds = RateLimiter::availableIn($this->throttleKey()); + + throw ValidationException::withMessages([ + 'form.email' => trans('auth.throttle', [ + 'seconds' => $seconds, + 'minutes' => ceil($seconds / 60), + ]), + ]); + } + + /** + * Get the authentication rate limiting throttle key. + */ + protected function throttleKey(): string + { + return Str::transliterate(Str::lower($this->email).'|'.request()->ip()); + } +} diff --git a/stubs/livewire-common/pest-tests/Feature/Auth/AuthenticationTest.php b/stubs/livewire-common/pest-tests/Feature/Auth/AuthenticationTest.php new file mode 100644 index 0000000..ec9910f --- /dev/null +++ b/stubs/livewire-common/pest-tests/Feature/Auth/AuthenticationTest.php @@ -0,0 +1,72 @@ +get('/login'); + + $response + ->assertOk() + ->assertSeeVolt('pages.auth.login'); +}); + +test('users can authenticate using the login screen', function () { + $user = User::factory()->create(); + + $component = Volt::test('pages.auth.login') + ->set('form.email', $user->email) + ->set('form.password', 'password'); + + $component->call('login'); + + $component + ->assertHasNoErrors() + ->assertRedirect(route('dashboard', absolute: false)); + + $this->assertAuthenticated(); +}); + +test('users can not authenticate with invalid password', function () { + $user = User::factory()->create(); + + $component = Volt::test('pages.auth.login') + ->set('form.email', $user->email) + ->set('form.password', 'wrong-password'); + + $component->call('login'); + + $component + ->assertHasErrors() + ->assertNoRedirect(); + + $this->assertGuest(); +}); + +test('navigation menu can be rendered', function () { + $user = User::factory()->create(); + + $this->actingAs($user); + + $response = $this->get('/dashboard'); + + $response + ->assertOk() + ->assertSeeVolt('layout.navigation'); +}); + +test('users can logout', function () { + $user = User::factory()->create(); + + $this->actingAs($user); + + $component = Volt::test('layout.navigation'); + + $component->call('logout'); + + $component + ->assertHasNoErrors() + ->assertRedirect('/'); + + $this->assertGuest(); +}); diff --git a/stubs/livewire-common/pest-tests/Feature/Auth/EmailVerificationTest.php b/stubs/livewire-common/pest-tests/Feature/Auth/EmailVerificationTest.php new file mode 100644 index 0000000..f282dff --- /dev/null +++ b/stubs/livewire-common/pest-tests/Feature/Auth/EmailVerificationTest.php @@ -0,0 +1,46 @@ +unverified()->create(); + + $response = $this->actingAs($user)->get('/verify-email'); + + $response->assertStatus(200); +}); + +test('email can be verified', function () { + $user = User::factory()->unverified()->create(); + + Event::fake(); + + $verificationUrl = URL::temporarySignedRoute( + 'verification.verify', + now()->addMinutes(60), + ['id' => $user->id, 'hash' => sha1($user->email)] + ); + + $response = $this->actingAs($user)->get($verificationUrl); + + Event::assertDispatched(Verified::class); + expect($user->fresh()->hasVerifiedEmail())->toBeTrue(); + $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); +}); + +test('email is not verified with invalid hash', function () { + $user = User::factory()->unverified()->create(); + + $verificationUrl = URL::temporarySignedRoute( + 'verification.verify', + now()->addMinutes(60), + ['id' => $user->id, 'hash' => sha1('wrong-email')] + ); + + $this->actingAs($user)->get($verificationUrl); + + expect($user->fresh()->hasVerifiedEmail())->toBeFalse(); +}); diff --git a/stubs/livewire-common/pest-tests/Feature/Auth/PasswordConfirmationTest.php b/stubs/livewire-common/pest-tests/Feature/Auth/PasswordConfirmationTest.php new file mode 100644 index 0000000..21c28c3 --- /dev/null +++ b/stubs/livewire-common/pest-tests/Feature/Auth/PasswordConfirmationTest.php @@ -0,0 +1,46 @@ +create(); + + $response = $this->actingAs($user)->get('/confirm-password'); + + $response + ->assertSeeVolt('pages.auth.confirm-password') + ->assertStatus(200); +}); + +test('password can be confirmed', function () { + $user = User::factory()->create(); + + $this->actingAs($user); + + $component = Volt::test('pages.auth.confirm-password') + ->set('password', 'password'); + + $component->call('confirmPassword'); + + $component + ->assertRedirect('/dashboard') + ->assertHasNoErrors(); +}); + +test('password is not confirmed with invalid password', function () { + $user = User::factory()->create(); + + $this->actingAs($user); + + $component = Volt::test('pages.auth.confirm-password') + ->set('password', 'wrong-password'); + + $component->call('confirmPassword'); + + $component + ->assertNoRedirect() + ->assertHasErrors('password'); +}); diff --git a/stubs/livewire-common/pest-tests/Feature/Auth/PasswordResetTest.php b/stubs/livewire-common/pest-tests/Feature/Auth/PasswordResetTest.php new file mode 100644 index 0000000..c478bce --- /dev/null +++ b/stubs/livewire-common/pest-tests/Feature/Auth/PasswordResetTest.php @@ -0,0 +1,73 @@ +get('/forgot-password'); + + $response + ->assertSeeVolt('pages.auth.forgot-password') + ->assertStatus(200); +}); + +test('reset password link can be requested', function () { + Notification::fake(); + + $user = User::factory()->create(); + + Volt::test('pages.auth.forgot-password') + ->set('email', $user->email) + ->call('sendPasswordResetLink'); + + Notification::assertSentTo($user, ResetPassword::class); +}); + +test('reset password screen can be rendered', function () { + Notification::fake(); + + $user = User::factory()->create(); + + Volt::test('pages.auth.forgot-password') + ->set('email', $user->email) + ->call('sendPasswordResetLink'); + + Notification::assertSentTo($user, ResetPassword::class, function ($notification) { + $response = $this->get('/reset-password/'.$notification->token); + + $response + ->assertSeeVolt('pages.auth.reset-password') + ->assertStatus(200); + + return true; + }); +}); + +test('password can be reset with valid token', function () { + Notification::fake(); + + $user = User::factory()->create(); + + Volt::test('pages.auth.forgot-password') + ->set('email', $user->email) + ->call('sendPasswordResetLink'); + + Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { + $component = Volt::test('pages.auth.reset-password', ['token' => $notification->token]) + ->set('email', $user->email) + ->set('password', 'password') + ->set('password_confirmation', 'password'); + + $component->call('resetPassword'); + + $component + ->assertRedirect('/login') + ->assertHasNoErrors(); + + return true; + }); +}); diff --git a/stubs/livewire-common/pest-tests/Feature/Auth/PasswordUpdateTest.php b/stubs/livewire-common/pest-tests/Feature/Auth/PasswordUpdateTest.php new file mode 100644 index 0000000..33b1d4b --- /dev/null +++ b/stubs/livewire-common/pest-tests/Feature/Auth/PasswordUpdateTest.php @@ -0,0 +1,41 @@ +create(); + + $this->actingAs($user); + + $component = Volt::test('profile.update-password-form') + ->set('current_password', 'password') + ->set('password', 'new-password') + ->set('password_confirmation', 'new-password') + ->call('updatePassword'); + + $component + ->assertHasNoErrors() + ->assertNoRedirect(); + + $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); +}); + +test('correct password must be provided to update password', function () { + $user = User::factory()->create(); + + $this->actingAs($user); + + $component = Volt::test('profile.update-password-form') + ->set('current_password', 'wrong-password') + ->set('password', 'new-password') + ->set('password_confirmation', 'new-password') + ->call('updatePassword'); + + $component + ->assertHasErrors(['current_password']) + ->assertNoRedirect(); +}); diff --git a/stubs/livewire-common/pest-tests/Feature/Auth/RegistrationTest.php b/stubs/livewire-common/pest-tests/Feature/Auth/RegistrationTest.php new file mode 100644 index 0000000..5d50039 --- /dev/null +++ b/stubs/livewire-common/pest-tests/Feature/Auth/RegistrationTest.php @@ -0,0 +1,27 @@ +get('/register'); + + $response + ->assertOk() + ->assertSeeVolt('pages.auth.register'); +}); + +test('new users can register', function () { + $component = Volt::test('pages.auth.register') + ->set('name', 'Test User') + ->set('email', 'test@example.com') + ->set('password', 'password') + ->set('password_confirmation', 'password'); + + $component->call('register'); + + $component->assertRedirect(route('dashboard', absolute: false)); + + $this->assertAuthenticated(); +}); diff --git a/stubs/livewire-common/pest-tests/Feature/ExampleTest.php b/stubs/livewire-common/pest-tests/Feature/ExampleTest.php new file mode 100644 index 0000000..8b5843f --- /dev/null +++ b/stubs/livewire-common/pest-tests/Feature/ExampleTest.php @@ -0,0 +1,7 @@ +get('/'); + + $response->assertStatus(200); +}); diff --git a/stubs/livewire-common/pest-tests/Feature/ProfileTest.php b/stubs/livewire-common/pest-tests/Feature/ProfileTest.php new file mode 100644 index 0000000..984f050 --- /dev/null +++ b/stubs/livewire-common/pest-tests/Feature/ProfileTest.php @@ -0,0 +1,89 @@ +create(); + + $this->actingAs($user); + + $response = $this->get('/profile'); + + $response + ->assertOk() + ->assertSeeVolt('profile.update-profile-information-form') + ->assertSeeVolt('profile.update-password-form') + ->assertSeeVolt('profile.delete-user-form'); +}); + +test('profile information can be updated', function () { + $user = User::factory()->create(); + + $this->actingAs($user); + + $component = Volt::test('profile.update-profile-information-form') + ->set('name', 'Test User') + ->set('email', 'test@example.com') + ->call('updateProfileInformation'); + + $component + ->assertHasNoErrors() + ->assertNoRedirect(); + + $user->refresh(); + + $this->assertSame('Test User', $user->name); + $this->assertSame('test@example.com', $user->email); + $this->assertNull($user->email_verified_at); +}); + +test('email verification status is unchanged when the email address is unchanged', function () { + $user = User::factory()->create(); + + $this->actingAs($user); + + $component = Volt::test('profile.update-profile-information-form') + ->set('name', 'Test User') + ->set('email', $user->email) + ->call('updateProfileInformation'); + + $component + ->assertHasNoErrors() + ->assertNoRedirect(); + + $this->assertNotNull($user->refresh()->email_verified_at); +}); + +test('user can delete their account', function () { + $user = User::factory()->create(); + + $this->actingAs($user); + + $component = Volt::test('profile.delete-user-form') + ->set('password', 'password') + ->call('deleteUser'); + + $component + ->assertHasNoErrors() + ->assertRedirect('/'); + + $this->assertGuest(); + $this->assertNull($user->fresh()); +}); + +test('correct password must be provided to delete account', function () { + $user = User::factory()->create(); + + $this->actingAs($user); + + $component = Volt::test('profile.delete-user-form') + ->set('password', 'wrong-password') + ->call('deleteUser'); + + $component + ->assertHasErrors('password') + ->assertNoRedirect(); + + $this->assertNotNull($user->fresh()); +}); diff --git a/stubs/livewire-common/pest-tests/Pest.php b/stubs/livewire-common/pest-tests/Pest.php new file mode 100644 index 0000000..40d096b --- /dev/null +++ b/stubs/livewire-common/pest-tests/Pest.php @@ -0,0 +1,47 @@ +extend(Tests\TestCase::class) + ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) + ->in('Feature'); + +/* +|-------------------------------------------------------------------------- +| Expectations +|-------------------------------------------------------------------------- +| +| When you're writing tests, you often need to check that values meet certain conditions. The +| "expect()" function gives you access to a set of "expectations" methods that you can use +| to assert different things. Of course, you may extend the Expectation API at any time. +| +*/ + +expect()->extend('toBeOne', function () { + return $this->toBe(1); +}); + +/* +|-------------------------------------------------------------------------- +| Functions +|-------------------------------------------------------------------------- +| +| While Pest is very powerful out-of-the-box, you may have some testing code specific to your +| project that you don't want to repeat in every file. Here you can also expose helpers as +| global functions to help you to reduce the number of lines of code in your test files. +| +*/ + +function something() +{ + // .. +} diff --git a/stubs/livewire-common/pest-tests/Unit/ExampleTest.php b/stubs/livewire-common/pest-tests/Unit/ExampleTest.php new file mode 100644 index 0000000..44a4f33 --- /dev/null +++ b/stubs/livewire-common/pest-tests/Unit/ExampleTest.php @@ -0,0 +1,5 @@ +toBeTrue(); +}); diff --git a/stubs/livewire-common/resources/views/components/action-message.blade.php b/stubs/livewire-common/resources/views/components/action-message.blade.php new file mode 100644 index 0000000..fe21e63 --- /dev/null +++ b/stubs/livewire-common/resources/views/components/action-message.blade.php @@ -0,0 +1,10 @@ +@props(['on']) + +
merge(['class' => 'text-sm text-gray-600 dark:text-gray-400']) }}> + {{ $slot->isEmpty() ? __('Saved.') : $slot }} +
diff --git a/stubs/livewire-common/resources/views/dashboard.blade.php b/stubs/livewire-common/resources/views/dashboard.blade.php new file mode 100644 index 0000000..d21afaa --- /dev/null +++ b/stubs/livewire-common/resources/views/dashboard.blade.php @@ -0,0 +1,17 @@ + + +

+ {{ __('Dashboard') }} +

+
+ +
+
+
+
+ {{ __("You're logged in!") }} +
+
+
+
+
diff --git a/stubs/livewire-common/resources/views/layouts/app.blade.php b/stubs/livewire-common/resources/views/layouts/app.blade.php new file mode 100644 index 0000000..89c7cd5 --- /dev/null +++ b/stubs/livewire-common/resources/views/layouts/app.blade.php @@ -0,0 +1,36 @@ + + + + + + + + {{ config('app.name', 'Laravel') }} + + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + +
+ + + + @if (isset($header)) +
+
+ {{ $header }} +
+
+ @endif + + +
+ {{ $slot }} +
+
+ + diff --git a/stubs/livewire-common/resources/views/layouts/guest.blade.php b/stubs/livewire-common/resources/views/layouts/guest.blade.php new file mode 100644 index 0000000..9e3d023 --- /dev/null +++ b/stubs/livewire-common/resources/views/layouts/guest.blade.php @@ -0,0 +1,30 @@ + + + + + + + + {{ config('app.name', 'Laravel') }} + + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + +
+
+ + + +
+ +
+ {{ $slot }} +
+
+ + diff --git a/stubs/livewire-common/resources/views/profile.blade.php b/stubs/livewire-common/resources/views/profile.blade.php new file mode 100644 index 0000000..5629a26 --- /dev/null +++ b/stubs/livewire-common/resources/views/profile.blade.php @@ -0,0 +1,29 @@ + + +

+ {{ __('Profile') }} +

+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
diff --git a/stubs/livewire-common/resources/views/welcome.blade.php b/stubs/livewire-common/resources/views/welcome.blade.php new file mode 100644 index 0000000..0464add --- /dev/null +++ b/stubs/livewire-common/resources/views/welcome.blade.php @@ -0,0 +1,145 @@ + + + + + + + Laravel + + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + +
+ +
+ +
+
+ + diff --git a/stubs/livewire-common/routes/auth.php b/stubs/livewire-common/routes/auth.php new file mode 100644 index 0000000..7c70b85 --- /dev/null +++ b/stubs/livewire-common/routes/auth.php @@ -0,0 +1,31 @@ +group(function () { + Volt::route('register', 'pages.auth.register') + ->name('register'); + + Volt::route('login', 'pages.auth.login') + ->name('login'); + + Volt::route('forgot-password', 'pages.auth.forgot-password') + ->name('password.request'); + + Volt::route('reset-password/{token}', 'pages.auth.reset-password') + ->name('password.reset'); +}); + +Route::middleware('auth')->group(function () { + Volt::route('verify-email', 'pages.auth.verify-email') + ->name('verification.notice'); + + Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) + ->middleware(['signed', 'throttle:6,1']) + ->name('verification.verify'); + + Volt::route('confirm-password', 'pages.auth.confirm-password') + ->name('password.confirm'); +}); diff --git a/stubs/livewire-common/routes/web.php b/stubs/livewire-common/routes/web.php new file mode 100644 index 0000000..50c7ff0 --- /dev/null +++ b/stubs/livewire-common/routes/web.php @@ -0,0 +1,15 @@ +middleware(['auth', 'verified']) + ->name('dashboard'); + +Route::view('profile', 'auth::profile') + ->middleware(['auth']) + ->name('profile'); + +require __DIR__.'/auth.php'; diff --git a/stubs/livewire-common/tests/Feature/Auth/AuthenticationTest.php b/stubs/livewire-common/tests/Feature/Auth/AuthenticationTest.php new file mode 100644 index 0000000..bf77746 --- /dev/null +++ b/stubs/livewire-common/tests/Feature/Auth/AuthenticationTest.php @@ -0,0 +1,86 @@ +get('/login'); + + $response + ->assertOk() + ->assertSeeVolt('pages.auth.login'); + } + + public function test_users_can_authenticate_using_the_login_screen(): void + { + $user = User::factory()->create(); + + $component = Volt::test('pages.auth.login') + ->set('form.email', $user->email) + ->set('form.password', 'password'); + + $component->call('login'); + + $component + ->assertHasNoErrors() + ->assertRedirect(route('dashboard', absolute: false)); + + $this->assertAuthenticated(); + } + + public function test_users_can_not_authenticate_with_invalid_password(): void + { + $user = User::factory()->create(); + + $component = Volt::test('pages.auth.login') + ->set('form.email', $user->email) + ->set('form.password', 'wrong-password'); + + $component->call('login'); + + $component + ->assertHasErrors() + ->assertNoRedirect(); + + $this->assertGuest(); + } + + public function test_navigation_menu_can_be_rendered(): void + { + $user = User::factory()->create(); + + $this->actingAs($user); + + $response = $this->get('/dashboard'); + + $response + ->assertOk() + ->assertSeeVolt('layout.navigation'); + } + + public function test_users_can_logout(): void + { + $user = User::factory()->create(); + + $this->actingAs($user); + + $component = Volt::test('layout.navigation'); + + $component->call('logout'); + + $component + ->assertHasNoErrors() + ->assertRedirect('/'); + + $this->assertGuest(); + } +} diff --git a/stubs/livewire-common/tests/Feature/Auth/EmailVerificationTest.php b/stubs/livewire-common/tests/Feature/Auth/EmailVerificationTest.php new file mode 100644 index 0000000..1236983 --- /dev/null +++ b/stubs/livewire-common/tests/Feature/Auth/EmailVerificationTest.php @@ -0,0 +1,60 @@ +unverified()->create(); + + $response = $this->actingAs($user)->get('/verify-email'); + + $response + ->assertSeeVolt('pages.auth.verify-email') + ->assertStatus(200); + } + + public function test_email_can_be_verified(): void + { + $user = User::factory()->unverified()->create(); + + Event::fake(); + + $verificationUrl = URL::temporarySignedRoute( + 'verification.verify', + now()->addMinutes(60), + ['id' => $user->id, 'hash' => sha1($user->email)] + ); + + $response = $this->actingAs($user)->get($verificationUrl); + + Event::assertDispatched(Verified::class); + $this->assertTrue($user->fresh()->hasVerifiedEmail()); + $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); + } + + public function test_email_is_not_verified_with_invalid_hash(): void + { + $user = User::factory()->unverified()->create(); + + $verificationUrl = URL::temporarySignedRoute( + 'verification.verify', + now()->addMinutes(60), + ['id' => $user->id, 'hash' => sha1('wrong-email')] + ); + + $this->actingAs($user)->get($verificationUrl); + + $this->assertFalse($user->fresh()->hasVerifiedEmail()); + } +} diff --git a/stubs/livewire-common/tests/Feature/Auth/PasswordConfirmationTest.php b/stubs/livewire-common/tests/Feature/Auth/PasswordConfirmationTest.php new file mode 100644 index 0000000..e91c6bb --- /dev/null +++ b/stubs/livewire-common/tests/Feature/Auth/PasswordConfirmationTest.php @@ -0,0 +1,56 @@ +create(); + + $response = $this->actingAs($user)->get('/confirm-password'); + + $response + ->assertSeeVolt('pages.auth.confirm-password') + ->assertStatus(200); + } + + public function test_password_can_be_confirmed(): void + { + $user = User::factory()->create(); + + $this->actingAs($user); + + $component = Volt::test('pages.auth.confirm-password') + ->set('password', 'password'); + + $component->call('confirmPassword'); + + $component + ->assertRedirect('/dashboard') + ->assertHasNoErrors(); + } + + public function test_password_is_not_confirmed_with_invalid_password(): void + { + $user = User::factory()->create(); + + $this->actingAs($user); + + $component = Volt::test('pages.auth.confirm-password') + ->set('password', 'wrong-password'); + + $component->call('confirmPassword'); + + $component + ->assertNoRedirect() + ->assertHasErrors('password'); + } +} diff --git a/stubs/livewire-common/tests/Feature/Auth/PasswordResetTest.php b/stubs/livewire-common/tests/Feature/Auth/PasswordResetTest.php new file mode 100644 index 0000000..f5bc765 --- /dev/null +++ b/stubs/livewire-common/tests/Feature/Auth/PasswordResetTest.php @@ -0,0 +1,84 @@ +get('/forgot-password'); + + $response + ->assertSeeVolt('pages.auth.forgot-password') + ->assertStatus(200); + } + + public function test_reset_password_link_can_be_requested(): void + { + Notification::fake(); + + $user = User::factory()->create(); + + Volt::test('pages.auth.forgot-password') + ->set('email', $user->email) + ->call('sendPasswordResetLink'); + + Notification::assertSentTo($user, ResetPassword::class); + } + + public function test_reset_password_screen_can_be_rendered(): void + { + Notification::fake(); + + $user = User::factory()->create(); + + Volt::test('pages.auth.forgot-password') + ->set('email', $user->email) + ->call('sendPasswordResetLink'); + + Notification::assertSentTo($user, ResetPassword::class, function ($notification) { + $response = $this->get('/reset-password/'.$notification->token); + + $response + ->assertSeeVolt('pages.auth.reset-password') + ->assertStatus(200); + + return true; + }); + } + + public function test_password_can_be_reset_with_valid_token(): void + { + Notification::fake(); + + $user = User::factory()->create(); + + Volt::test('pages.auth.forgot-password') + ->set('email', $user->email) + ->call('sendPasswordResetLink'); + + Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { + $component = Volt::test('pages.auth.reset-password', ['token' => $notification->token]) + ->set('email', $user->email) + ->set('password', 'password') + ->set('password_confirmation', 'password'); + + $component->call('resetPassword'); + + $component + ->assertRedirect('/login') + ->assertHasNoErrors(); + + return true; + }); + } +} diff --git a/stubs/livewire-common/tests/Feature/Auth/PasswordUpdateTest.php b/stubs/livewire-common/tests/Feature/Auth/PasswordUpdateTest.php new file mode 100644 index 0000000..d24c295 --- /dev/null +++ b/stubs/livewire-common/tests/Feature/Auth/PasswordUpdateTest.php @@ -0,0 +1,50 @@ +create(); + + $this->actingAs($user); + + $component = Volt::test('profile.update-password-form') + ->set('current_password', 'password') + ->set('password', 'new-password') + ->set('password_confirmation', 'new-password') + ->call('updatePassword'); + + $component + ->assertHasNoErrors() + ->assertNoRedirect(); + + $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); + } + + public function test_correct_password_must_be_provided_to_update_password(): void + { + $user = User::factory()->create(); + + $this->actingAs($user); + + $component = Volt::test('profile.update-password-form') + ->set('current_password', 'wrong-password') + ->set('password', 'new-password') + ->set('password_confirmation', 'new-password') + ->call('updatePassword'); + + $component + ->assertHasErrors(['current_password']) + ->assertNoRedirect(); + } +} diff --git a/stubs/livewire-common/tests/Feature/Auth/RegistrationTest.php b/stubs/livewire-common/tests/Feature/Auth/RegistrationTest.php new file mode 100644 index 0000000..16100aa --- /dev/null +++ b/stubs/livewire-common/tests/Feature/Auth/RegistrationTest.php @@ -0,0 +1,36 @@ +get('/register'); + + $response + ->assertOk() + ->assertSeeVolt('pages.auth.register'); + } + + public function test_new_users_can_register(): void + { + $component = Volt::test('pages.auth.register') + ->set('name', 'Test User') + ->set('email', 'test@example.com') + ->set('password', 'password') + ->set('password_confirmation', 'password'); + + $component->call('register'); + + $component->assertRedirect(route('dashboard', absolute: false)); + + $this->assertAuthenticated(); + } +} diff --git a/stubs/livewire-common/tests/Feature/ProfileTest.php b/stubs/livewire-common/tests/Feature/ProfileTest.php new file mode 100644 index 0000000..52fae1b --- /dev/null +++ b/stubs/livewire-common/tests/Feature/ProfileTest.php @@ -0,0 +1,101 @@ +create(); + + $response = $this->actingAs($user)->get('/profile'); + + $response + ->assertOk() + ->assertSeeVolt('profile.update-profile-information-form') + ->assertSeeVolt('profile.update-password-form') + ->assertSeeVolt('profile.delete-user-form'); + } + + public function test_profile_information_can_be_updated(): void + { + $user = User::factory()->create(); + + $this->actingAs($user); + + $component = Volt::test('profile.update-profile-information-form') + ->set('name', 'Test User') + ->set('email', 'test@example.com') + ->call('updateProfileInformation'); + + $component + ->assertHasNoErrors() + ->assertNoRedirect(); + + $user->refresh(); + + $this->assertSame('Test User', $user->name); + $this->assertSame('test@example.com', $user->email); + $this->assertNull($user->email_verified_at); + } + + public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged(): void + { + $user = User::factory()->create(); + + $this->actingAs($user); + + $component = Volt::test('profile.update-profile-information-form') + ->set('name', 'Test User') + ->set('email', $user->email) + ->call('updateProfileInformation'); + + $component + ->assertHasNoErrors() + ->assertNoRedirect(); + + $this->assertNotNull($user->refresh()->email_verified_at); + } + + public function test_user_can_delete_their_account(): void + { + $user = User::factory()->create(); + + $this->actingAs($user); + + $component = Volt::test('profile.delete-user-form') + ->set('password', 'password') + ->call('deleteUser'); + + $component + ->assertHasNoErrors() + ->assertRedirect('/'); + + $this->assertGuest(); + $this->assertNull($user->fresh()); + } + + public function test_correct_password_must_be_provided_to_delete_account(): void + { + $user = User::factory()->create(); + + $this->actingAs($user); + + $component = Volt::test('profile.delete-user-form') + ->set('password', 'wrong-password') + ->call('deleteUser'); + + $component + ->assertHasErrors('password') + ->assertNoRedirect(); + + $this->assertNotNull($user->fresh()); + } +} diff --git a/stubs/livewire-functional/resources/views/livewire/layout/navigation.blade.php b/stubs/livewire-functional/resources/views/livewire/layout/navigation.blade.php new file mode 100644 index 0000000..c593139 --- /dev/null +++ b/stubs/livewire-functional/resources/views/livewire/layout/navigation.blade.php @@ -0,0 +1,104 @@ +redirect('/', navigate: true); +}; + +?> + + diff --git a/stubs/livewire-functional/resources/views/livewire/pages/auth/confirm-password.blade.php b/stubs/livewire-functional/resources/views/livewire/pages/auth/confirm-password.blade.php new file mode 100644 index 0000000..c9a9604 --- /dev/null +++ b/stubs/livewire-functional/resources/views/livewire/pages/auth/confirm-password.blade.php @@ -0,0 +1,61 @@ + '']); + +rules(['password' => ['required', 'string']]); + +$confirmPassword = function () { + $this->validate(); + + if (! Auth::guard('web')->validate([ + 'email' => Auth::user()->email, + 'password' => $this->password, + ])) { + throw ValidationException::withMessages([ + 'password' => __('auth.password'), + ]); + } + + session(['auth.password_confirmed_at' => time()]); + + $this->redirectIntended(default: route('dashboard', absolute: false), navigate: true); +}; + +?> + +
+
+ {{ __('This is a secure area of the application. Please confirm your password before continuing.') }} +
+ +
+ +
+ + + + + +
+ +
+ + {{ __('Confirm') }} + +
+
+
diff --git a/stubs/livewire-functional/resources/views/livewire/pages/auth/forgot-password.blade.php b/stubs/livewire-functional/resources/views/livewire/pages/auth/forgot-password.blade.php new file mode 100644 index 0000000..fcf865a --- /dev/null +++ b/stubs/livewire-functional/resources/views/livewire/pages/auth/forgot-password.blade.php @@ -0,0 +1,61 @@ + '']); + +rules(['email' => ['required', 'string', 'email']]); + +$sendPasswordResetLink = function () { + $this->validate(); + + // We will send the password reset link to this user. Once we have attempted + // to send the link, we will examine the response then see the message we + // need to show to the user. Finally, we'll send out a proper response. + $status = Password::sendResetLink( + $this->only('email') + ); + + if ($status != Password::RESET_LINK_SENT) { + $this->addError('email', __($status)); + + return; + } + + $this->reset('email'); + + Session::flash('status', __($status)); +}; + +?> + +
+
+ {{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }} +
+ + + + +
+ +
+ + + +
+ +
+ + {{ __('Email Password Reset Link') }} + +
+
+
diff --git a/stubs/livewire-functional/resources/views/livewire/pages/auth/login.blade.php b/stubs/livewire-functional/resources/views/livewire/pages/auth/login.blade.php new file mode 100644 index 0000000..e8ccb0d --- /dev/null +++ b/stubs/livewire-functional/resources/views/livewire/pages/auth/login.blade.php @@ -0,0 +1,69 @@ +validate(); + + $this->form->authenticate(); + + Session::regenerate(); + + $this->redirectIntended(default: route('dashboard', absolute: false), navigate: true); +}; + +?> + +
+ + + +
+ +
+ + + +
+ + +
+ + + + + +
+ + +
+ +
+ +
+ @if (Route::has('password.request')) + + {{ __('Forgot your password?') }} + + @endif + + + {{ __('Log in') }} + +
+
+
diff --git a/stubs/livewire-functional/resources/views/livewire/pages/auth/register.blade.php b/stubs/livewire-functional/resources/views/livewire/pages/auth/register.blade.php new file mode 100644 index 0000000..e0cee0e --- /dev/null +++ b/stubs/livewire-functional/resources/views/livewire/pages/auth/register.blade.php @@ -0,0 +1,91 @@ + '', + 'email' => '', + 'password' => '', + 'password_confirmation' => '' +]); + +rules([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], + 'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()], +]); + +$register = function () { + $validated = $this->validate(); + + $validated['password'] = Hash::make($validated['password']); + + event(new Registered($user = User::create($validated))); + + Auth::login($user); + + $this->redirect(route('dashboard', absolute: false), navigate: true); +}; + +?> + +
+
+ +
+ + + +
+ + +
+ + + +
+ + +
+ + + + + +
+ + +
+ + + + + +
+ +
+ + {{ __('Already registered?') }} + + + + {{ __('Register') }} + +
+
+
diff --git a/stubs/livewire-functional/resources/views/livewire/pages/auth/reset-password.blade.php b/stubs/livewire-functional/resources/views/livewire/pages/auth/reset-password.blade.php new file mode 100644 index 0000000..1907c5c --- /dev/null +++ b/stubs/livewire-functional/resources/views/livewire/pages/auth/reset-password.blade.php @@ -0,0 +1,97 @@ +locked(); + +state([ + 'email' => fn () => request()->string('email')->value(), + 'password' => '', + 'password_confirmation' => '' +]); + +rules([ + 'token' => ['required'], + 'email' => ['required', 'string', 'email'], + 'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()], +]); + +$resetPassword = function () { + $this->validate(); + + // Here we will attempt to reset the user's password. If it is successful we + // will update the password on an actual user model and persist it to the + // database. Otherwise we will parse the error and return the response. + $status = Password::reset( + $this->only('email', 'password', 'password_confirmation', 'token'), + function ($user) { + $user->forceFill([ + 'password' => Hash::make($this->password), + 'remember_token' => Str::random(60), + ])->save(); + + event(new PasswordReset($user)); + } + ); + + // If the password was successfully reset, we will redirect the user back to + // the application's home authenticated view. If there is an error we can + // redirect them back to where they came from with their error message. + if ($status != Password::PASSWORD_RESET) { + $this->addError('email', __($status)); + + return; + } + + Session::flash('status', __($status)); + + $this->redirectRoute('login', navigate: true); +}; + +?> + +
+
+ +
+ + + +
+ + +
+ + + +
+ + +
+ + + + + +
+ +
+ + {{ __('Reset Password') }} + +
+
+
diff --git a/stubs/livewire-functional/resources/views/livewire/pages/auth/verify-email.blade.php b/stubs/livewire-functional/resources/views/livewire/pages/auth/verify-email.blade.php new file mode 100644 index 0000000..380a193 --- /dev/null +++ b/stubs/livewire-functional/resources/views/livewire/pages/auth/verify-email.blade.php @@ -0,0 +1,51 @@ +hasVerifiedEmail()) { + $this->redirectIntended(default: route('dashboard', absolute: false), navigate: true); + + return; + } + + Auth::user()->sendEmailVerificationNotification(); + + Session::flash('status', 'verification-link-sent'); +}; + +$logout = function (Logout $logout) { + $logout(); + + $this->redirect('/', navigate: true); +}; + +?> + +
+
+ {{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }} +
+ + @if (session('status') == 'verification-link-sent') +
+ {{ __('A new verification link has been sent to the email address you provided during registration.') }} +
+ @endif + +
+ + {{ __('Resend Verification Email') }} + + + +
+
diff --git a/stubs/livewire-functional/resources/views/livewire/profile/delete-user-form.blade.php b/stubs/livewire-functional/resources/views/livewire/profile/delete-user-form.blade.php new file mode 100644 index 0000000..bb070a7 --- /dev/null +++ b/stubs/livewire-functional/resources/views/livewire/profile/delete-user-form.blade.php @@ -0,0 +1,76 @@ + '']); + +rules(['password' => ['required', 'string', 'current_password']]); + +$deleteUser = function (Logout $logout) { + $this->validate(); + + tap(Auth::user(), $logout(...))->delete(); + + $this->redirect('/', navigate: true); +}; + +?> + +
+
+

+ {{ __('Delete Account') }} +

+ +

+ {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }} +

+
+ + {{ __('Delete Account') }} + + +
+ +

+ {{ __('Are you sure you want to delete your account?') }} +

+ +

+ {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }} +

+ +
+ + + + + +
+ +
+ + {{ __('Cancel') }} + + + + {{ __('Delete Account') }} + +
+
+
+
diff --git a/stubs/livewire-functional/resources/views/livewire/profile/update-password-form.blade.php b/stubs/livewire-functional/resources/views/livewire/profile/update-password-form.blade.php new file mode 100644 index 0000000..207d2d8 --- /dev/null +++ b/stubs/livewire-functional/resources/views/livewire/profile/update-password-form.blade.php @@ -0,0 +1,80 @@ + '', + 'password' => '', + 'password_confirmation' => '' +]); + +rules([ + 'current_password' => ['required', 'string', 'current_password'], + 'password' => ['required', 'string', Password::defaults(), 'confirmed'], +]); + +$updatePassword = function () { + try { + $validated = $this->validate(); + } catch (ValidationException $e) { + $this->reset('current_password', 'password', 'password_confirmation'); + + throw $e; + } + + Auth::user()->update([ + 'password' => Hash::make($validated['password']), + ]); + + $this->reset('current_password', 'password', 'password_confirmation'); + + $this->dispatch('password-updated'); +}; + +?> + +
+
+

+ {{ __('Update Password') }} +

+ +

+ {{ __('Ensure your account is using a long, random password to stay secure.') }} +

+
+ +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ {{ __('Save') }} + + + {{ __('Saved.') }} + +
+
+
diff --git a/stubs/livewire-functional/resources/views/livewire/profile/update-profile-information-form.blade.php b/stubs/livewire-functional/resources/views/livewire/profile/update-profile-information-form.blade.php new file mode 100644 index 0000000..6bd0137 --- /dev/null +++ b/stubs/livewire-functional/resources/views/livewire/profile/update-profile-information-form.blade.php @@ -0,0 +1,101 @@ +{{moduleNameLower}}:: fn () => auth()->user()->name, + 'email' => fn () => auth()->user()->email +]); + +$updateProfileInformation = function () { + $user = Auth::user(); + + $validated = $this->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', Rule::unique(User::class)->ignore($user->id)], + ]); + + $user->fill($validated); + + if ($user->isDirty('email')) { + $user->email_verified_at = null; + } + + $user->save(); + + $this->dispatch('profile-updated', name: $user->name); +}; + +$sendVerification = function () { + $user = Auth::user(); + + if ($user->hasVerifiedEmail()) { + $this->redirectIntended(default: route('dashboard', absolute: false)); + + return; + } + + $user->sendEmailVerificationNotification(); + + Session::flash('status', 'verification-link-sent'); +}; + +?> + +
+
+

+ {{ __('Profile Information') }} +

+ +

+ {{ __("Update your account's profile information and email address.") }} +

+
+ +
+
+ + + +
+ +
+ + + + + @if (auth()->user() instanceof MustVerifyEmail && ! auth()->user()->hasVerifiedEmail()) +
+

+ {{ __('Your email address is unverified.') }} + + +

+ + @if (session('status') === 'verification-link-sent') +

+ {{ __('A new verification link has been sent to your email address.') }} +

+ @endif +
+ @endif +
+ +
+ {{ __('Save') }} + + + {{ __('Saved.') }} + +
+
+
diff --git a/stubs/livewire-functional/resources/views/livewire/welcome/navigation.blade.php b/stubs/livewire-functional/resources/views/livewire/welcome/navigation.blade.php new file mode 100644 index 0000000..7d079de --- /dev/null +++ b/stubs/livewire-functional/resources/views/livewire/welcome/navigation.blade.php @@ -0,0 +1,26 @@ + diff --git a/stubs/livewire/resources/views/livewire/layout/navigation.blade.php b/stubs/livewire/resources/views/livewire/layout/navigation.blade.php new file mode 100644 index 0000000..05f0059 --- /dev/null +++ b/stubs/livewire/resources/views/livewire/layout/navigation.blade.php @@ -0,0 +1,110 @@ +redirect('/', navigate: true); + } +}; ?> + + diff --git a/stubs/livewire/resources/views/livewire/pages/auth/confirm-password.blade.php b/stubs/livewire/resources/views/livewire/pages/auth/confirm-password.blade.php new file mode 100644 index 0000000..544802a --- /dev/null +++ b/stubs/livewire/resources/views/livewire/pages/auth/confirm-password.blade.php @@ -0,0 +1,62 @@ +validate([ + 'password' => ['required', 'string'], + ]); + + if (! Auth::guard('web')->validate([ + 'email' => Auth::user()->email, + 'password' => $this->password, + ])) { + throw ValidationException::withMessages([ + 'password' => __('auth.password'), + ]); + } + + session(['auth.password_confirmed_at' => time()]); + + $this->redirectIntended(default: route('dashboard', absolute: false), navigate: true); + } +}; ?> + +
+
+ {{ __('This is a secure area of the application. Please confirm your password before continuing.') }} +
+ +
+ +
+ + + + + +
+ +
+ + {{ __('Confirm') }} + +
+
+
diff --git a/stubs/livewire/resources/views/livewire/pages/auth/forgot-password.blade.php b/stubs/livewire/resources/views/livewire/pages/auth/forgot-password.blade.php new file mode 100644 index 0000000..1924c3c --- /dev/null +++ b/stubs/livewire/resources/views/livewire/pages/auth/forgot-password.blade.php @@ -0,0 +1,61 @@ +validate([ + 'email' => ['required', 'string', 'email'], + ]); + + // We will send the password reset link to this user. Once we have attempted + // to send the link, we will examine the response then see the message we + // need to show to the user. Finally, we'll send out a proper response. + $status = Password::sendResetLink( + $this->only('email') + ); + + if ($status != Password::RESET_LINK_SENT) { + $this->addError('email', __($status)); + + return; + } + + $this->reset('email'); + + session()->flash('status', __($status)); + } +}; ?> + +
+
+ {{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }} +
+ + + + +
+ +
+ + + +
+ +
+ + {{ __('Email Password Reset Link') }} + +
+
+
diff --git a/stubs/livewire/resources/views/livewire/pages/auth/login.blade.php b/stubs/livewire/resources/views/livewire/pages/auth/login.blade.php new file mode 100644 index 0000000..afc18c8 --- /dev/null +++ b/stubs/livewire/resources/views/livewire/pages/auth/login.blade.php @@ -0,0 +1,71 @@ +validate(); + + $this->form->authenticate(); + + Session::regenerate(); + + $this->redirectIntended(default: route('dashboard', absolute: false), navigate: true); + } +}; ?> + +
+ + + +
+ +
+ + + +
+ + +
+ + + + + +
+ + +
+ +
+ +
+ @if (Route::has('password.request')) + + {{ __('Forgot your password?') }} + + @endif + + + {{ __('Log in') }} + +
+
+
diff --git a/stubs/livewire/resources/views/livewire/pages/auth/register.blade.php b/stubs/livewire/resources/views/livewire/pages/auth/register.blade.php new file mode 100644 index 0000000..b473fa7 --- /dev/null +++ b/stubs/livewire/resources/views/livewire/pages/auth/register.blade.php @@ -0,0 +1,88 @@ +validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], + 'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()], + ]); + + $validated['password'] = Hash::make($validated['password']); + + event(new Registered($user = User::create($validated))); + + Auth::login($user); + + $this->redirect(route('dashboard', absolute: false), navigate: true); + } +}; ?> + +
+
+ +
+ + + +
+ + +
+ + + +
+ + +
+ + + + + +
+ + +
+ + + + + +
+ +
+ + {{ __('Already registered?') }} + + + + {{ __('Register') }} + +
+
+
diff --git a/stubs/livewire/resources/views/livewire/pages/auth/reset-password.blade.php b/stubs/livewire/resources/views/livewire/pages/auth/reset-password.blade.php new file mode 100644 index 0000000..931c178 --- /dev/null +++ b/stubs/livewire/resources/views/livewire/pages/auth/reset-password.blade.php @@ -0,0 +1,105 @@ +token = $token; + + $this->email = request()->string('email'); + } + + /** + * Reset the password for the given user. + */ + public function resetPassword(): void + { + $this->validate([ + 'token' => ['required'], + 'email' => ['required', 'string', 'email'], + 'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()], + ]); + + // Here we will attempt to reset the user's password. If it is successful we + // will update the password on an actual user model and persist it to the + // database. Otherwise we will parse the error and return the response. + $status = Password::reset( + $this->only('email', 'password', 'password_confirmation', 'token'), + function ($user) { + $user->forceFill([ + 'password' => Hash::make($this->password), + 'remember_token' => Str::random(60), + ])->save(); + + event(new PasswordReset($user)); + } + ); + + // If the password was successfully reset, we will redirect the user back to + // the application's home authenticated view. If there is an error we can + // redirect them back to where they came from with their error message. + if ($status != Password::PASSWORD_RESET) { + $this->addError('email', __($status)); + + return; + } + + Session::flash('status', __($status)); + + $this->redirectRoute('login', navigate: true); + } +}; ?> + +
+
+ +
+ + + +
+ + +
+ + + +
+ + +
+ + + + + +
+ +
+ + {{ __('Reset Password') }} + +
+
+
diff --git a/stubs/livewire/resources/views/livewire/pages/auth/verify-email.blade.php b/stubs/livewire/resources/views/livewire/pages/auth/verify-email.blade.php new file mode 100644 index 0000000..3e66883 --- /dev/null +++ b/stubs/livewire/resources/views/livewire/pages/auth/verify-email.blade.php @@ -0,0 +1,58 @@ +hasVerifiedEmail()) { + $this->redirectIntended(default: route('dashboard', absolute: false), navigate: true); + + return; + } + + Auth::user()->sendEmailVerificationNotification(); + + Session::flash('status', 'verification-link-sent'); + } + + /** + * Log the current user out of the application. + */ + public function logout(Logout $logout): void + { + $logout(); + + $this->redirect('/', navigate: true); + } +}; ?> + +
+
+ {{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }} +
+ + @if (session('status') == 'verification-link-sent') +
+ {{ __('A new verification link has been sent to the email address you provided during registration.') }} +
+ @endif + +
+ + {{ __('Resend Verification Email') }} + + + +
+
diff --git a/stubs/livewire/resources/views/livewire/profile/delete-user-form.blade.php b/stubs/livewire/resources/views/livewire/profile/delete-user-form.blade.php new file mode 100644 index 0000000..0568b8a --- /dev/null +++ b/stubs/livewire/resources/views/livewire/profile/delete-user-form.blade.php @@ -0,0 +1,79 @@ +validate([ + 'password' => ['required', 'string', 'current_password'], + ]); + + tap(Auth::user(), $logout(...))->delete(); + + $this->redirect('/', navigate: true); + } +}; ?> + +
+
+

+ {{ __('Delete Account') }} +

+ +

+ {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }} +

+
+ + {{ __('Delete Account') }} + + +
+ +

+ {{ __('Are you sure you want to delete your account?') }} +

+ +

+ {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }} +

+ +
+ + + + + +
+ +
+ + {{ __('Cancel') }} + + + + {{ __('Delete Account') }} + +
+
+
+
diff --git a/stubs/livewire/resources/views/livewire/profile/update-password-form.blade.php b/stubs/livewire/resources/views/livewire/profile/update-password-form.blade.php new file mode 100644 index 0000000..e65fd17 --- /dev/null +++ b/stubs/livewire/resources/views/livewire/profile/update-password-form.blade.php @@ -0,0 +1,79 @@ +validate([ + 'current_password' => ['required', 'string', 'current_password'], + 'password' => ['required', 'string', Password::defaults(), 'confirmed'], + ]); + } catch (ValidationException $e) { + $this->reset('current_password', 'password', 'password_confirmation'); + + throw $e; + } + + Auth::user()->update([ + 'password' => Hash::make($validated['password']), + ]); + + $this->reset('current_password', 'password', 'password_confirmation'); + + $this->dispatch('password-updated'); + } +}; ?> + +
+
+

+ {{ __('Update Password') }} +

+ +

+ {{ __('Ensure your account is using a long, random password to stay secure.') }} +

+
+ +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ {{ __('Save') }} + + + {{ __('Saved.') }} + +
+
+
diff --git a/stubs/livewire/resources/views/livewire/profile/update-profile-information-form.blade.php b/stubs/livewire/resources/views/livewire/profile/update-profile-information-form.blade.php new file mode 100644 index 0000000..c24d327 --- /dev/null +++ b/stubs/livewire/resources/views/livewire/profile/update-profile-information-form.blade.php @@ -0,0 +1,115 @@ +{{moduleNameLower}}::name = Auth::user()->name; + $this->email = Auth::user()->email; + } + + /** + * Update the profile information for the currently authenticated user. + */ + public function updateProfileInformation(): void + { + $user = Auth::user(); + + $validated = $this->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', Rule::unique(User::class)->ignore($user->id)], + ]); + + $user->fill($validated); + + if ($user->isDirty('email')) { + $user->email_verified_at = null; + } + + $user->save(); + + $this->dispatch('profile-updated', name: $user->name); + } + + /** + * Send an email verification notification to the current user. + */ + public function sendVerification(): void + { + $user = Auth::user(); + + if ($user->hasVerifiedEmail()) { + $this->redirectIntended(default: route('dashboard', absolute: false)); + + return; + } + + $user->sendEmailVerificationNotification(); + + Session::flash('status', 'verification-link-sent'); + } +}; ?> + +
+
+

+ {{ __('Profile Information') }} +

+ +

+ {{ __("Update your account's profile information and email address.") }} +

+
+ +
+
+ + + +
+ +
+ + + + + @if (auth()->user() instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! auth()->user()->hasVerifiedEmail()) +
+

+ {{ __('Your email address is unverified.') }} + + +

+ + @if (session('status') === 'verification-link-sent') +

+ {{ __('A new verification link has been sent to your email address.') }} +

+ @endif +
+ @endif +
+ +
+ {{ __('Save') }} + + + {{ __('Saved.') }} + +
+
+
diff --git a/stubs/livewire/resources/views/livewire/welcome/navigation.blade.php b/stubs/livewire/resources/views/livewire/welcome/navigation.blade.php new file mode 100644 index 0000000..7d079de --- /dev/null +++ b/stubs/livewire/resources/views/livewire/welcome/navigation.blade.php @@ -0,0 +1,26 @@ +