From e660da46fb2a793ac429b6d7f189a1e66a7b9ba7 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Thu, 15 Jan 2026 21:55:24 +0100 Subject: [PATCH] refactor: rebase on Livewire 4 starter kit --- app/Actions/Fortify/CreateNewUser.php | 33 ++ app/Actions/Fortify/ResetUserPassword.php | 29 ++ app/Concerns/PasswordValidationRules.php | 28 ++ app/Concerns/ProfileValidationRules.php | 50 +++ app/Livewire/Actions/DeviceAutoJoin.php | 2 +- app/Models/User.php | 3 +- app/Providers/AppServiceProvider.php | 3 +- app/Providers/FortifyServiceProvider.php | 72 ++++ bootstrap/providers.php | 1 + composer.json | 2 +- composer.lock | 232 ++++++----- config/auth.php | 2 +- config/cache.php | 13 +- config/database.php | 17 +- config/filesystems.php | 5 +- config/fortify.php | 159 +++++++ config/logging.php | 10 +- config/mail.php | 4 +- config/queue.php | 19 +- config/services.php | 10 +- config/session.php | 12 +- database/factories/UserFactory.php | 16 +- ..._add_two_factor_columns_to_users_table.php | 34 ++ .../views/components/action-message.blade.php | 2 +- .../views/components/auth-header.blade.php | 6 +- .../components/desktop-user-menu.blade.php | 39 ++ .../components/settings/layout.blade.php | 22 - resources/views/flux/navlist/group.blade.php | 6 +- resources/views/layouts/app/sidebar.blade.php | 96 +++++ resources/views/layouts/auth.blade.php | 4 +- resources/views/layouts/auth/card.blade.php | 4 +- resources/views/layouts/auth/simple.blade.php | 2 +- resources/views/layouts/auth/split.blade.php | 12 +- .../livewire/auth/confirm-password.blade.php | 62 --- .../livewire/auth/forgot-password.blade.php | 45 -- resources/views/livewire/auth/login.blade.php | 153 ------- .../views/livewire/auth/register.blade.php | 98 ----- .../livewire/auth/reset-password.blade.php | 121 ------ .../livewire/auth/verify-email.blade.php | 62 --- .../livewire/settings/appearance.blade.php | 20 - .../settings/delete-user-form.blade.php | 61 --- .../pages/auth/confirm-password.blade.php | 28 ++ .../pages/auth/forgot-password.blade.php | 31 ++ resources/views/pages/auth/login.blade.php | 85 ++++ resources/views/pages/auth/register.blade.php | 67 +++ .../views/pages/auth/reset-password.blade.php | 52 +++ .../pages/auth/two-factor-challenge.blade.php | 95 +++++ .../views/pages/auth/verify-email.blade.php | 29 ++ .../views/pages/settings/appearance.blade.php | 22 + .../pages/settings/delete-user-form.blade.php | 64 +++ .../views/pages/settings/layout.blade.php | 27 ++ .../settings/password.blade.php | 34 +- .../settings/preferences.blade.php | 4 +- .../settings/profile.blade.php | 67 +-- .../settings/support.blade.php | 4 +- .../views/pages/settings/two-factor.blade.php | 388 ++++++++++++++++++ .../two-factor/recovery-codes.blade.php | 136 ++++++ .../views/partials/settings-heading.blade.php | 4 +- routes/auth.php | 31 +- routes/settings.php | 23 ++ routes/web.php | 7 +- tests/Feature/Auth/AuthenticationTest.php | 10 +- tests/Feature/Auth/EmailVerificationTest.php | 4 +- .../Feature/Auth/PasswordConfirmationTest.php | 30 +- tests/Feature/Auth/PasswordResetTest.php | 29 +- tests/Feature/Auth/RegistrationTest.php | 19 +- tests/Feature/Auth/TwoFactorChallengeTest.php | 34 ++ tests/Feature/Settings/PasswordUpdateTest.php | 6 +- tests/Feature/Settings/ProfileUpdateTest.php | 8 +- 69 files changed, 1967 insertions(+), 942 deletions(-) create mode 100644 app/Actions/Fortify/CreateNewUser.php create mode 100644 app/Actions/Fortify/ResetUserPassword.php create mode 100644 app/Concerns/PasswordValidationRules.php create mode 100644 app/Concerns/ProfileValidationRules.php create mode 100644 app/Providers/FortifyServiceProvider.php create mode 100644 config/fortify.php create mode 100644 database/migrations/2026_01_15_075243_add_two_factor_columns_to_users_table.php create mode 100644 resources/views/components/desktop-user-menu.blade.php delete mode 100644 resources/views/components/settings/layout.blade.php create mode 100644 resources/views/layouts/app/sidebar.blade.php delete mode 100644 resources/views/livewire/auth/confirm-password.blade.php delete mode 100644 resources/views/livewire/auth/forgot-password.blade.php delete mode 100644 resources/views/livewire/auth/login.blade.php delete mode 100644 resources/views/livewire/auth/register.blade.php delete mode 100644 resources/views/livewire/auth/reset-password.blade.php delete mode 100644 resources/views/livewire/auth/verify-email.blade.php delete mode 100644 resources/views/livewire/settings/appearance.blade.php delete mode 100644 resources/views/livewire/settings/delete-user-form.blade.php create mode 100644 resources/views/pages/auth/confirm-password.blade.php create mode 100644 resources/views/pages/auth/forgot-password.blade.php create mode 100644 resources/views/pages/auth/login.blade.php create mode 100644 resources/views/pages/auth/register.blade.php create mode 100644 resources/views/pages/auth/reset-password.blade.php create mode 100644 resources/views/pages/auth/two-factor-challenge.blade.php create mode 100644 resources/views/pages/auth/verify-email.blade.php create mode 100644 resources/views/pages/settings/appearance.blade.php create mode 100644 resources/views/pages/settings/delete-user-form.blade.php create mode 100644 resources/views/pages/settings/layout.blade.php rename resources/views/{livewire => pages}/settings/password.blade.php (65%) rename resources/views/{livewire => pages}/settings/preferences.blade.php (95%) rename resources/views/{livewire => pages}/settings/profile.blade.php (56%) rename resources/views/{livewire => pages}/settings/support.blade.php (91%) create mode 100644 resources/views/pages/settings/two-factor.blade.php create mode 100644 resources/views/pages/settings/two-factor/recovery-codes.blade.php create mode 100644 routes/settings.php create mode 100644 tests/Feature/Auth/TwoFactorChallengeTest.php diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php new file mode 100644 index 0000000..3c7c00c --- /dev/null +++ b/app/Actions/Fortify/CreateNewUser.php @@ -0,0 +1,33 @@ + $input + */ + public function create(array $input): User + { + Validator::make($input, [ + ...$this->profileRules(), + 'password' => $this->passwordRules(), + ])->validate(); + + return User::create([ + 'name' => $input['name'], + 'email' => $input['email'], + 'password' => $input['password'], + ]); + } +} diff --git a/app/Actions/Fortify/ResetUserPassword.php b/app/Actions/Fortify/ResetUserPassword.php new file mode 100644 index 0000000..8fda5dd --- /dev/null +++ b/app/Actions/Fortify/ResetUserPassword.php @@ -0,0 +1,29 @@ + $input + */ + public function reset(User $user, array $input): void + { + Validator::make($input, [ + 'password' => $this->passwordRules(), + ])->validate(); + + $user->forceFill([ + 'password' => $input['password'], + ])->save(); + } +} diff --git a/app/Concerns/PasswordValidationRules.php b/app/Concerns/PasswordValidationRules.php new file mode 100644 index 0000000..9b45ef0 --- /dev/null +++ b/app/Concerns/PasswordValidationRules.php @@ -0,0 +1,28 @@ +|string> + */ + protected function passwordRules(): array + { + return ['required', 'string', Password::default(), 'confirmed']; + } + + /** + * Get the validation rules used to validate the current password. + * + * @return array|string> + */ + protected function currentPasswordRules(): array + { + return ['required', 'string', 'current_password']; + } +} diff --git a/app/Concerns/ProfileValidationRules.php b/app/Concerns/ProfileValidationRules.php new file mode 100644 index 0000000..46e19ba --- /dev/null +++ b/app/Concerns/ProfileValidationRules.php @@ -0,0 +1,50 @@ +|string>> + */ + protected function profileRules(?int $userId = null): array + { + return [ + 'name' => $this->nameRules(), + 'email' => $this->emailRules($userId), + ]; + } + + /** + * Get the validation rules used to validate user names. + * + * @return array|string> + */ + protected function nameRules(): array + { + return ['required', 'string', 'max:255']; + } + + /** + * Get the validation rules used to validate user emails. + * + * @return array|string> + */ + protected function emailRules(?int $userId = null): array + { + return [ + 'required', + 'string', + 'email', + 'max:255', + $userId === null + ? Rule::unique(User::class) + : Rule::unique(User::class)->ignore($userId), + ]; + } +} diff --git a/app/Livewire/Actions/DeviceAutoJoin.php b/app/Livewire/Actions/DeviceAutoJoin.php index 183add4..82d63ed 100644 --- a/app/Livewire/Actions/DeviceAutoJoin.php +++ b/app/Livewire/Actions/DeviceAutoJoin.php @@ -12,7 +12,7 @@ class DeviceAutoJoin extends Component public function mount(): void { - $this->deviceAutojoin = auth()->user()->assign_new_devices; + $this->deviceAutojoin = (bool) (auth()->user()->assign_new_devices ?? false); $this->isFirstUser = auth()->user()->id === 1; } diff --git a/app/Models/User.php b/app/Models/User.php index c6d39b8..8d7aa7e 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -8,12 +8,13 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Str; +use Laravel\Fortify\TwoFactorAuthenticatable; use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable // implements MustVerifyEmail { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasApiTokens, HasFactory, Notifiable; + use HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable; /** * The attributes that are mass assignable. diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index b8ad9bb..78adbde 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use App\Services\OidcProvider; +use App\Services\QrCodeService; use Illuminate\Http\Request; use Illuminate\Support\Facades\URL; use Illuminate\Support\ServiceProvider; @@ -15,7 +16,7 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // + $this->app->bind('qr-code', fn (): QrCodeService => new QrCodeService); } /** diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php new file mode 100644 index 0000000..bf7a1b3 --- /dev/null +++ b/app/Providers/FortifyServiceProvider.php @@ -0,0 +1,72 @@ +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 (): Factory|View => view('pages::auth.login')); + Fortify::verifyEmailView(fn (): Factory|View => view('pages::auth.verify-email')); + Fortify::twoFactorChallengeView(fn (): Factory|View => view('pages::auth.two-factor-challenge')); + Fortify::confirmPasswordView(fn (): Factory|View => view('pages::auth.confirm-password')); + Fortify::registerView(fn (): Factory|View => view('pages::auth.register')); + Fortify::resetPasswordView(fn (): Factory|View => view('pages::auth.reset-password')); + Fortify::requestPasswordResetLinkView(fn (): Factory|View => view('pages::auth.forgot-password')); + } + + /** + * Configure rate limiting. + */ + private function configureRateLimiting(): void + { + RateLimiter::for('two-factor', fn (Request $request) => Limit::perMinute(5)->by($request->session()->get('login.id'))); + + RateLimiter::for('login', function (Request $request) { + $throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip()); + + return Limit::perMinute(5)->by($throttleKey); + }); + } +} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 38b258d..0ad9c57 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -2,4 +2,5 @@ return [ App\Providers\AppServiceProvider::class, + App\Providers\FortifyServiceProvider::class, ]; diff --git a/composer.json b/composer.json index 6639cb6..a4951a0 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "bnussbau/laravel-trmnl-blade": "2.1.*", "bnussbau/trmnl-pipeline-php": "^0.6.0", "keepsuit/laravel-liquid": "^0.5.2", + "laravel/fortify": "^1.30", "laravel/framework": "^12.1", "laravel/sanctum": "^4.0", "laravel/socialite": "^5.23", @@ -25,7 +26,6 @@ "livewire/flux": "^2.0", "livewire/livewire": "^4.0", "om/icalparser": "^3.2", - "simplesoftwareio/simple-qrcode": "^4.2", "spatie/browsershot": "^5.0", "stevebauman/purify": "^6.3", "symfony/yaml": "^7.3", diff --git a/composer.lock b/composer.lock index d5b94ad..52a9ed0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "dea01d6eda8d497162134bf44c2a3adb", + "content-hash": "cd2e6e87598cd99111e73d9ce8e0a9b8", "packages": [ { "name": "aws/aws-crt-php", @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.369.13", + "version": "3.369.14", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "bedc36250c92b8287be855a2d25427fb0e065483" + "reference": "b40eb1177d2e621c18cd797ca6cc9efb5a0e99d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/bedc36250c92b8287be855a2d25427fb0e065483", - "reference": "bedc36250c92b8287be855a2d25427fb0e065483", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b40eb1177d2e621c18cd797ca6cc9efb5a0e99d9", + "reference": "b40eb1177d2e621c18cd797ca6cc9efb5a0e99d9", "shasum": "" }, "require": { @@ -153,34 +153,35 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.369.13" + "source": "https://github.com/aws/aws-sdk-php/tree/3.369.14" }, - "time": "2026-01-14T19:13:46+00:00" + "time": "2026-01-15T19:10:54+00:00" }, { "name": "bacon/bacon-qr-code", - "version": "2.0.8", + "version": "v3.0.3", "source": { "type": "git", "url": "https://github.com/Bacon/BaconQrCode.git", - "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22" + "reference": "36a1cb2b81493fa5b82e50bf8068bf84d1542563" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22", - "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/36a1cb2b81493fa5b82e50bf8068bf84d1542563", + "reference": "36a1cb2b81493fa5b82e50bf8068bf84d1542563", "shasum": "" }, "require": { "dasprid/enum": "^1.0.3", "ext-iconv": "*", - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "phly/keep-a-changelog": "^2.1", - "phpunit/phpunit": "^7 | ^8 | ^9", - "spatie/phpunit-snapshot-assertions": "^4.2.9", - "squizlabs/php_codesniffer": "^3.4" + "phly/keep-a-changelog": "^2.12", + "phpunit/phpunit": "^10.5.11 || ^11.0.4", + "spatie/phpunit-snapshot-assertions": "^5.1.5", + "spatie/pixelmatch-php": "^1.2.0", + "squizlabs/php_codesniffer": "^3.9" }, "suggest": { "ext-imagick": "to generate QR code images" @@ -207,9 +208,9 @@ "homepage": "https://github.com/Bacon/BaconQrCode", "support": { "issues": "https://github.com/Bacon/BaconQrCode/issues", - "source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8" + "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.3" }, - "time": "2022-12-07T17:46:57+00:00" + "time": "2025-11-19T17:15:36+00:00" }, { "name": "bnussbau/laravel-trmnl-blade", @@ -1780,6 +1781,69 @@ }, "time": "2025-12-01T12:01:51+00:00" }, + { + "name": "laravel/fortify", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/fortify.git", + "reference": "e0666dabeec0b6428678af1d51f436dcfb24e3a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/fortify/zipball/e0666dabeec0b6428678af1d51f436dcfb24e3a9", + "reference": "e0666dabeec0b6428678af1d51f436dcfb24e3a9", + "shasum": "" + }, + "require": { + "bacon/bacon-qr-code": "^3.0", + "ext-json": "*", + "illuminate/support": "^10.0|^11.0|^12.0", + "php": "^8.1", + "pragmarx/google2fa": "^9.0", + "symfony/console": "^6.0|^7.0" + }, + "require-dev": { + "orchestra/testbench": "^8.36|^9.15|^10.8", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Fortify\\FortifyServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Fortify\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Backend controllers and scaffolding for Laravel authentication.", + "keywords": [ + "auth", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/fortify/issues", + "source": "https://github.com/laravel/fortify" + }, + "time": "2025-12-15T14:48:33+00:00" + }, { "name": "laravel/framework", "version": "v12.47.0", @@ -2004,16 +2068,16 @@ }, { "name": "laravel/prompts", - "version": "v0.3.9", + "version": "v0.3.10", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "5c41bf0555b7cfefaad4e66d3046675829581ac4" + "reference": "360ba095ef9f51017473505191fbd4ab73e1cab3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/5c41bf0555b7cfefaad4e66d3046675829581ac4", - "reference": "5c41bf0555b7cfefaad4e66d3046675829581ac4", + "url": "https://api.github.com/repos/laravel/prompts/zipball/360ba095ef9f51017473505191fbd4ab73e1cab3", + "reference": "360ba095ef9f51017473505191fbd4ab73e1cab3", "shasum": "" }, "require": { @@ -2057,9 +2121,9 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.9" + "source": "https://github.com/laravel/prompts/tree/v0.3.10" }, - "time": "2026-01-07T21:00:29+00:00" + "time": "2026-01-13T20:29:29+00:00" }, { "name": "laravel/sanctum", @@ -4162,6 +4226,58 @@ ], "time": "2025-12-15T11:51:42+00:00" }, + { + "name": "pragmarx/google2fa", + "version": "v9.0.0", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/google2fa.git", + "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/e6bc62dd6ae83acc475f57912e27466019a1f2cf", + "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1.0|^2.0|^3.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^7.5.15|^8.5|^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PragmaRX\\Google2FA\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "role": "Creator & Designer" + } + ], + "description": "A One Time Password Authentication package, compatible with Google Authenticator.", + "keywords": [ + "2fa", + "Authentication", + "Two Factor Authentication", + "google2fa" + ], + "support": { + "issues": "https://github.com/antonioribeiro/google2fa/issues", + "source": "https://github.com/antonioribeiro/google2fa/tree/v9.0.0" + }, + "time": "2025-09-19T22:51:08+00:00" + }, { "name": "psr/clock", "version": "1.0.0", @@ -4851,74 +4967,6 @@ }, "time": "2025-12-14T04:43:48+00:00" }, - { - "name": "simplesoftwareio/simple-qrcode", - "version": "4.2.0", - "source": { - "type": "git", - "url": "https://github.com/SimpleSoftwareIO/simple-qrcode.git", - "reference": "916db7948ca6772d54bb617259c768c9cdc8d537" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/SimpleSoftwareIO/simple-qrcode/zipball/916db7948ca6772d54bb617259c768c9cdc8d537", - "reference": "916db7948ca6772d54bb617259c768c9cdc8d537", - "shasum": "" - }, - "require": { - "bacon/bacon-qr-code": "^2.0", - "ext-gd": "*", - "php": ">=7.2|^8.0" - }, - "require-dev": { - "mockery/mockery": "~1", - "phpunit/phpunit": "~9" - }, - "suggest": { - "ext-imagick": "Allows the generation of PNG QrCodes.", - "illuminate/support": "Allows for use within Laravel." - }, - "type": "library", - "extra": { - "laravel": { - "aliases": { - "QrCode": "SimpleSoftwareIO\\QrCode\\Facades\\QrCode" - }, - "providers": [ - "SimpleSoftwareIO\\QrCode\\QrCodeServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "SimpleSoftwareIO\\QrCode\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Simple Software LLC", - "email": "support@simplesoftware.io" - } - ], - "description": "Simple QrCode is a QR code generator made for Laravel.", - "homepage": "https://www.simplesoftware.io/#/docs/simple-qrcode", - "keywords": [ - "Simple", - "generator", - "laravel", - "qrcode", - "wrapper" - ], - "support": { - "issues": "https://github.com/SimpleSoftwareIO/simple-qrcode/issues", - "source": "https://github.com/SimpleSoftwareIO/simple-qrcode/tree/4.2.0" - }, - "time": "2021-02-08T20:43:55+00:00" - }, { "name": "spatie/browsershot", "version": "5.2.0", diff --git a/config/auth.php b/config/auth.php index 0ba5d5d..7d1eb0d 100644 --- a/config/auth.php +++ b/config/auth.php @@ -104,7 +104,7 @@ return [ | Password Confirmation Timeout |-------------------------------------------------------------------------- | - | Here you may define the amount of seconds before a password confirmation + | Here you may define the number of seconds before a password confirmation | window expires and users are asked to re-enter their password via the | confirmation screen. By default, the timeout lasts for three hours. | diff --git a/config/cache.php b/config/cache.php index 925f7d2..b32aead 100644 --- a/config/cache.php +++ b/config/cache.php @@ -27,7 +27,8 @@ return [ | same cache driver to group types of items stored in your caches. | | Supported drivers: "array", "database", "file", "memcached", - | "redis", "dynamodb", "octane", "null" + | "redis", "dynamodb", "octane", + | "failover", "null" | */ @@ -90,6 +91,14 @@ return [ 'driver' => 'octane', ], + 'failover' => [ + 'driver' => 'failover', + 'stores' => [ + 'database', + 'array', + ], + ], + ], /* @@ -103,6 +112,6 @@ return [ | */ - 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), ]; diff --git a/config/database.php b/config/database.php index 8910562..4da9dc6 100644 --- a/config/database.php +++ b/config/database.php @@ -40,6 +40,7 @@ return [ 'busy_timeout' => null, 'journal_mode' => null, 'synchronous' => null, + 'transaction_mode' => 'DEFERRED', ], 'mysql' => [ @@ -58,7 +59,7 @@ return [ 'strict' => true, 'engine' => null, 'options' => extension_loaded('pdo_mysql') ? array_filter([ - PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + (PHP_VERSION_ID >= 80500 ? Pdo\Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), ]) : [], ], @@ -78,7 +79,7 @@ return [ 'strict' => true, 'engine' => null, 'options' => extension_loaded('pdo_mysql') ? array_filter([ - PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + (PHP_VERSION_ID >= 80500 ? Pdo\Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), ]) : [], ], @@ -94,7 +95,7 @@ return [ 'prefix' => '', 'prefix_indexes' => true, 'search_path' => 'public', - 'sslmode' => 'prefer', + 'sslmode' => env('DB_SSLMODE', 'prefer'), ], 'sqlsrv' => [ @@ -147,7 +148,7 @@ return [ 'options' => [ 'cluster' => env('REDIS_CLUSTER', 'redis'), - 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), + 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'), 'persistent' => env('REDIS_PERSISTENT', false), ], @@ -158,6 +159,10 @@ return [ 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_DB', '0'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), ], 'cache' => [ @@ -167,6 +172,10 @@ return [ 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_CACHE_DB', '1'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), ], ], diff --git a/config/filesystems.php b/config/filesystems.php index b564035..ccaf2a9 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -35,14 +35,16 @@ return [ 'root' => storage_path('app/private'), 'serve' => true, 'throw' => false, + 'report' => false, ], 'public' => [ 'driver' => 'local', 'root' => storage_path('app/public'), - 'url' => env('APP_URL').'/storage', + 'url' => mb_rtrim(env('APP_URL'), '/').'/storage', 'visibility' => 'public', 'throw' => false, + 'report' => false, ], 's3' => [ @@ -55,6 +57,7 @@ return [ 'endpoint' => env('AWS_ENDPOINT'), 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 'throw' => false, + 'report' => false, ], ], diff --git a/config/fortify.php b/config/fortify.php new file mode 100644 index 0000000..ed7b0c0 --- /dev/null +++ b/config/fortify.php @@ -0,0 +1,159 @@ + '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' => '/dashboard', + + /* + |-------------------------------------------------------------------------- + | 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 + | 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' => [ + config('app.registration.enabled') && Features::registration(), + Features::resetPasswords(), + Features::emailVerification(), + // Features::updateProfileInformation(), + // Features::updatePasswords(), + Features::twoFactorAuthentication([ + 'confirm' => true, + 'confirmPassword' => true, + // 'window' => 0, + ]), + ], + +]; diff --git a/config/logging.php b/config/logging.php index 47b1d08..9f6e543 100644 --- a/config/logging.php +++ b/config/logging.php @@ -54,7 +54,7 @@ return [ 'stack' => [ 'driver' => 'stack', - 'channels' => explode(',', env('LOG_STACK', 'single')), + 'channels' => explode(',', (string) env('LOG_STACK', 'single')), 'ignore_exceptions' => false, ], @@ -98,10 +98,10 @@ return [ 'driver' => 'monolog', 'level' => env('LOG_LEVEL', 'debug'), 'handler' => StreamHandler::class, - 'formatter' => env('LOG_STDOUT_FORMATTER'), - 'with' => [ + 'handler_with' => [ 'stream' => 'php://stdout', ], + 'formatter' => env('LOG_STDOUT_FORMATTER'), 'processors' => [PsrLogMessageProcessor::class], ], @@ -109,10 +109,10 @@ return [ 'driver' => 'monolog', 'level' => env('LOG_LEVEL', 'debug'), 'handler' => StreamHandler::class, - 'formatter' => env('LOG_STDERR_FORMATTER'), - 'with' => [ + 'handler_with' => [ 'stream' => 'php://stderr', ], + 'formatter' => env('LOG_STDERR_FORMATTER'), 'processors' => [PsrLogMessageProcessor::class], ], diff --git a/config/mail.php b/config/mail.php index 756305b..522b284 100644 --- a/config/mail.php +++ b/config/mail.php @@ -46,7 +46,7 @@ return [ 'username' => env('MAIL_USERNAME'), 'password' => env('MAIL_PASSWORD'), 'timeout' => null, - 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), ], 'ses' => [ @@ -85,6 +85,7 @@ return [ 'smtp', 'log', ], + 'retry_after' => 60, ], 'roundrobin' => [ @@ -93,6 +94,7 @@ return [ 'ses', 'postmark', ], + 'retry_after' => 60, ], ], diff --git a/config/queue.php b/config/queue.php index 116bd8d..79c2c0a 100644 --- a/config/queue.php +++ b/config/queue.php @@ -24,7 +24,8 @@ return [ | used by your application. An example configuration is provided for | each backend supported by Laravel. You're also free to add more. | - | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", + | "deferred", "background", "failover", "null" | */ @@ -72,6 +73,22 @@ return [ 'after_commit' => false, ], + 'deferred' => [ + 'driver' => 'deferred', + ], + + 'background' => [ + 'driver' => 'background', + ], + + 'failover' => [ + 'driver' => 'failover', + 'connections' => [ + 'database', + 'deferred', + ], + ], + ], /* diff --git a/config/services.php b/config/services.php index d97255a..82eeeef 100644 --- a/config/services.php +++ b/config/services.php @@ -15,7 +15,11 @@ return [ */ 'postmark' => [ - 'token' => env('POSTMARK_TOKEN'), + 'key' => env('POSTMARK_API_KEY'), + ], + + 'resend' => [ + 'key' => env('RESEND_API_KEY'), ], 'ses' => [ @@ -24,10 +28,6 @@ return [ 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], - 'resend' => [ - 'key' => env('RESEND_KEY'), - ], - 'slack' => [ 'notifications' => [ 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), diff --git a/config/session.php b/config/session.php index f0b6541..5b541b7 100644 --- a/config/session.php +++ b/config/session.php @@ -13,8 +13,8 @@ return [ | incoming requests. Laravel supports a variety of storage options to | persist session data. Database storage is a great default choice. | - | Supported: "file", "cookie", "database", "apc", - | "memcached", "redis", "dynamodb", "array" + | Supported: "file", "cookie", "database", "memcached", + | "redis", "dynamodb", "array" | */ @@ -32,7 +32,7 @@ return [ | */ - 'lifetime' => env('SESSION_LIFETIME', 120), + 'lifetime' => (int) env('SESSION_LIFETIME', 120), 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), @@ -97,7 +97,7 @@ return [ | define the cache store which should be used to store the session data | between requests. This must match one of your defined cache stores. | - | Affects: "apc", "dynamodb", "memcached", "redis" + | Affects: "dynamodb", "memcached", "redis" | */ @@ -129,7 +129,7 @@ return [ 'cookie' => env( 'SESSION_COOKIE', - Str::slug(env('APP_NAME', 'laravel'), '_').'_session' + Str::slug((string) env('APP_NAME', 'laravel')).'-session' ), /* @@ -152,7 +152,7 @@ return [ | | This value determines the domain and subdomains the session cookie is | available to. By default, the cookie will be available to the root - | domain and all subdomains. Typically, this shouldn't be changed. + | domain without subdomains. Typically, this shouldn't be changed. | */ diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index c5d3f2c..80da5ac 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -29,7 +29,9 @@ class UserFactory extends Factory 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), 'remember_token' => Str::random(10), - 'assign_new_devices' => false, + 'two_factor_secret' => null, + 'two_factor_recovery_codes' => null, + 'two_factor_confirmed_at' => null, ]; } @@ -42,4 +44,16 @@ class UserFactory extends Factory 'email_verified_at' => null, ]); } + + /** + * Indicate that the model has two-factor authentication configured. + */ + public function withTwoFactor(): static + { + return $this->state(fn (array $attributes) => [ + 'two_factor_secret' => encrypt('secret'), + 'two_factor_recovery_codes' => encrypt(json_encode(['recovery-code-1'])), + 'two_factor_confirmed_at' => now(), + ]); + } } diff --git a/database/migrations/2026_01_15_075243_add_two_factor_columns_to_users_table.php b/database/migrations/2026_01_15_075243_add_two_factor_columns_to_users_table.php new file mode 100644 index 0000000..187d974 --- /dev/null +++ b/database/migrations/2026_01_15_075243_add_two_factor_columns_to_users_table.php @@ -0,0 +1,34 @@ +text('two_factor_secret')->after('password')->nullable(); + $table->text('two_factor_recovery_codes')->after('two_factor_secret')->nullable(); + $table->timestamp('two_factor_confirmed_at')->after('two_factor_recovery_codes')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn([ + 'two_factor_secret', + 'two_factor_recovery_codes', + 'two_factor_confirmed_at', + ]); + }); + } +}; diff --git a/resources/views/components/action-message.blade.php b/resources/views/components/action-message.blade.php index db63acf..d313ee6 100644 --- a/resources/views/components/action-message.blade.php +++ b/resources/views/components/action-message.blade.php @@ -8,7 +8,7 @@ x-show.transition.out.opacity.duration.1500ms="shown" x-transition:leave.opacity.duration.1500ms style="display: none" - {{ $attributes->merge(['class' => 'text-sm text-gray-600']) }} + {{ $attributes->merge(['class' => 'text-sm']) }} > {{ $slot->isEmpty() ? __('Saved.') : $slot }} diff --git a/resources/views/components/auth-header.blade.php b/resources/views/components/auth-header.blade.php index a68f69d..e596a3f 100644 --- a/resources/views/components/auth-header.blade.php +++ b/resources/views/components/auth-header.blade.php @@ -3,7 +3,7 @@ 'description', ]) -
-

{{ $title }}

-

{{ $description }}

+
+ {{ $title }} + {{ $description }}
diff --git a/resources/views/components/desktop-user-menu.blade.php b/resources/views/components/desktop-user-menu.blade.php new file mode 100644 index 0000000..5b386c5 --- /dev/null +++ b/resources/views/components/desktop-user-menu.blade.php @@ -0,0 +1,39 @@ + + only('name') }} + :initials="auth()->user()->initials()" + icon:trailing="chevrons-up-down" + data-test="sidebar-menu-button" + /> + + +
+ +
+ {{ auth()->user()->name }} + {{ auth()->user()->email }} +
+
+ + + + {{ __('Settings') }} + +
+ @csrf + + {{ __('Log Out') }} + +
+
+
+
diff --git a/resources/views/components/settings/layout.blade.php b/resources/views/components/settings/layout.blade.php deleted file mode 100644 index d0ed4cf..0000000 --- a/resources/views/components/settings/layout.blade.php +++ /dev/null @@ -1,22 +0,0 @@ -
-
- - Preferences - Profile - Password - Appearance - Support - -
- - - -
- {{ $heading ?? '' }} - {{ $subheading ?? '' }} - -
- {{ $slot }} -
-
-
diff --git a/resources/views/flux/navlist/group.blade.php b/resources/views/flux/navlist/group.blade.php index 844171f..1c94dfb 100644 --- a/resources/views/flux/navlist/group.blade.php +++ b/resources/views/flux/navlist/group.blade.php @@ -15,7 +15,7 @@ type="button" class="group/disclosure-button mb-[2px] flex h-10 w-full items-center rounded-lg text-zinc-500 hover:bg-zinc-800/5 hover:text-zinc-800 lg:h-8 dark:text-white/80 dark:hover:bg-white/[7%] dark:hover:text-white" > -
+
@@ -23,8 +23,8 @@ {{ $heading }} -