From a15645ad88870c9511fd49e8314a74fb421c184c Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Wed, 14 Jan 2026 23:59:00 +0100 Subject: [PATCH 1/7] refactor: upgrade to Livewire 4 --- app/Providers/VoltServiceProvider.php | 28 ------ bootstrap/providers.php | 2 - composer.json | 3 +- composer.lock | 85 ++----------------- .../views/components/layouts/auth.blade.php | 3 - .../{components => }/layouts/app.blade.php | 4 +- .../layouts/app/header.blade.php | 0 resources/views/layouts/auth.blade.php | 3 + .../layouts/auth/card.blade.php | 0 .../layouts/auth/simple.blade.php | 0 .../layouts/auth/split.blade.php | 0 .../livewire/auth/confirm-password.blade.php | 4 +- .../livewire/auth/forgot-password.blade.php | 4 +- resources/views/livewire/auth/login.blade.php | 12 +-- .../views/livewire/auth/register.blade.php | 4 +- .../livewire/auth/reset-password.blade.php | 4 +- .../livewire/auth/verify-email.blade.php | 4 +- .../views/livewire/catalog/index.blade.php | 2 +- .../views/livewire/catalog/trmnl.blade.php | 2 +- resources/views/livewire/codemirror.blade.php | 6 +- .../views/livewire/device-dashboard.blade.php | 2 +- .../livewire/device-models/index.blade.php | 2 +- .../livewire/device-palettes/index.blade.php | 2 +- .../livewire/devices/configure.blade.php | 2 +- .../views/livewire/devices/logs.blade.php | 2 +- .../views/livewire/devices/manage.blade.php | 2 +- .../views/livewire/playlists/index.blade.php | 2 +- .../views/livewire/plugins/api.blade.php | 2 +- .../livewire/plugins/config-modal.blade.php | 2 +- .../plugins/image-webhook-instance.blade.php | 8 +- .../livewire/plugins/image-webhook.blade.php | 4 +- .../views/livewire/plugins/index.blade.php | 2 +- .../views/livewire/plugins/markup.blade.php | 2 +- .../views/livewire/plugins/recipe.blade.php | 2 +- .../plugins/recipes/settings.blade.php | 2 +- .../livewire/settings/appearance.blade.php | 2 +- .../settings/delete-user-form.blade.php | 2 +- .../livewire/settings/password.blade.php | 2 +- .../livewire/settings/preferences.blade.php | 2 +- .../views/livewire/settings/profile.blade.php | 2 +- .../views/livewire/settings/support.blade.php | 5 ++ resources/views/welcome.blade.php | 6 +- routes/auth.php | 13 ++- routes/web.php | 37 ++++---- tests/Feature/Auth/AuthenticationTest.php | 3 +- .../Feature/Auth/PasswordConfirmationTest.php | 5 +- tests/Feature/Auth/PasswordResetTest.php | 9 +- tests/Feature/Auth/RegistrationTest.php | 4 +- tests/Feature/Devices/ManageTest.php | 11 ++- tests/Feature/Livewire/Catalog/IndexTest.php | 13 ++- .../Livewire/Plugins/ConfigModalTest.php | 11 ++- .../Livewire/Plugins/RecipeSettingsTest.php | 11 ++- tests/Feature/Settings/PasswordUpdateTest.php | 5 +- tests/Feature/Settings/ProfileUpdateTest.php | 9 +- tests/Feature/Volt/CatalogTrmnlTest.php | 19 ++--- tests/Feature/Volt/DevicePalettesTest.php | 65 +++++++------- 56 files changed, 166 insertions(+), 278 deletions(-) delete mode 100644 app/Providers/VoltServiceProvider.php delete mode 100644 resources/views/components/layouts/auth.blade.php rename resources/views/{components => }/layouts/app.blade.php (51%) rename resources/views/{components => }/layouts/app/header.blade.php (100%) create mode 100644 resources/views/layouts/auth.blade.php rename resources/views/{components => }/layouts/auth/card.blade.php (100%) rename resources/views/{components => }/layouts/auth/simple.blade.php (100%) rename resources/views/{components => }/layouts/auth/split.blade.php (100%) diff --git a/app/Providers/VoltServiceProvider.php b/app/Providers/VoltServiceProvider.php deleted file mode 100644 index e61d984..0000000 --- a/app/Providers/VoltServiceProvider.php +++ /dev/null @@ -1,28 +0,0 @@ - - {{ $slot }} - diff --git a/resources/views/components/layouts/app.blade.php b/resources/views/layouts/app.blade.php similarity index 51% rename from resources/views/components/layouts/app.blade.php rename to resources/views/layouts/app.blade.php index ec0ebf7..e20d56b 100644 --- a/resources/views/components/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -1,5 +1,5 @@ - + {{ $slot }} - + diff --git a/resources/views/components/layouts/app/header.blade.php b/resources/views/layouts/app/header.blade.php similarity index 100% rename from resources/views/components/layouts/app/header.blade.php rename to resources/views/layouts/app/header.blade.php diff --git a/resources/views/layouts/auth.blade.php b/resources/views/layouts/auth.blade.php new file mode 100644 index 0000000..86a1249 --- /dev/null +++ b/resources/views/layouts/auth.blade.php @@ -0,0 +1,3 @@ + + {{ $slot }} + diff --git a/resources/views/components/layouts/auth/card.blade.php b/resources/views/layouts/auth/card.blade.php similarity index 100% rename from resources/views/components/layouts/auth/card.blade.php rename to resources/views/layouts/auth/card.blade.php diff --git a/resources/views/components/layouts/auth/simple.blade.php b/resources/views/layouts/auth/simple.blade.php similarity index 100% rename from resources/views/components/layouts/auth/simple.blade.php rename to resources/views/layouts/auth/simple.blade.php diff --git a/resources/views/components/layouts/auth/split.blade.php b/resources/views/layouts/auth/split.blade.php similarity index 100% rename from resources/views/components/layouts/auth/split.blade.php rename to resources/views/layouts/auth/split.blade.php diff --git a/resources/views/livewire/auth/confirm-password.blade.php b/resources/views/livewire/auth/confirm-password.blade.php index 62a86d8..faa24e5 100644 --- a/resources/views/livewire/auth/confirm-password.blade.php +++ b/resources/views/livewire/auth/confirm-password.blade.php @@ -3,9 +3,9 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Validation\ValidationException; use Livewire\Attributes\Layout; -use Livewire\Volt\Component; +use Livewire\Component; -new #[Layout('components.layouts.auth')] class extends Component { +new #[Layout('layouts.auth')] class extends Component { public string $password = ''; /** diff --git a/resources/views/livewire/auth/forgot-password.blade.php b/resources/views/livewire/auth/forgot-password.blade.php index f31727b..158c3fc 100644 --- a/resources/views/livewire/auth/forgot-password.blade.php +++ b/resources/views/livewire/auth/forgot-password.blade.php @@ -2,9 +2,9 @@ use Illuminate\Support\Facades\Password; use Livewire\Attributes\Layout; -use Livewire\Volt\Component; +use Livewire\Component; -new #[Layout('components.layouts.auth')] class extends Component { +new #[Layout('layouts.auth')] class extends Component { public string $email = ''; /** diff --git a/resources/views/livewire/auth/login.blade.php b/resources/views/livewire/auth/login.blade.php index 6f8488a..645b866 100644 --- a/resources/views/livewire/auth/login.blade.php +++ b/resources/views/livewire/auth/login.blade.php @@ -8,9 +8,9 @@ use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; use Livewire\Attributes\Layout; use Livewire\Attributes\Validate; -use Livewire\Volt\Component; +use Livewire\Component; -new #[Layout('components.layouts.auth')] class extends Component { +new #[Layout('layouts.auth')] class extends Component { #[Validate('required|string|email')] public string $email = ''; @@ -131,10 +131,10 @@ new #[Layout('components.layouts.auth')] class extends Component {
- {{ __('Continue with OIDC') }} diff --git a/resources/views/livewire/auth/register.blade.php b/resources/views/livewire/auth/register.blade.php index 59964a5..e98a4e1 100644 --- a/resources/views/livewire/auth/register.blade.php +++ b/resources/views/livewire/auth/register.blade.php @@ -6,9 +6,9 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Illuminate\Validation\Rules; use Livewire\Attributes\Layout; -use Livewire\Volt\Component; +use Livewire\Component; -new #[Layout('components.layouts.auth')] class extends Component { +new #[Layout('layouts.auth')] class extends Component { public string $name = ''; public string $email = ''; public string $password = ''; diff --git a/resources/views/livewire/auth/reset-password.blade.php b/resources/views/livewire/auth/reset-password.blade.php index d7d9605..a58fd31 100644 --- a/resources/views/livewire/auth/reset-password.blade.php +++ b/resources/views/livewire/auth/reset-password.blade.php @@ -8,9 +8,9 @@ use Illuminate\Support\Str; use Illuminate\Validation\Rules; use Livewire\Attributes\Layout; use Livewire\Attributes\Locked; -use Livewire\Volt\Component; +use Livewire\Component; -new #[Layout('components.layouts.auth')] class extends Component { +new #[Layout('layouts.auth')] class extends Component { #[Locked] public string $token = ''; public string $email = ''; diff --git a/resources/views/livewire/auth/verify-email.blade.php b/resources/views/livewire/auth/verify-email.blade.php index f1ad9e0..c05e3c4 100644 --- a/resources/views/livewire/auth/verify-email.blade.php +++ b/resources/views/livewire/auth/verify-email.blade.php @@ -4,9 +4,9 @@ use App\Livewire\Actions\Logout; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Session; use Livewire\Attributes\Layout; -use Livewire\Volt\Component; +use Livewire\Component; -new #[Layout('components.layouts.auth')] class extends Component { +new #[Layout('layouts.auth')] class extends Component { /** * Send an email verification notification to the user. */ diff --git a/resources/views/livewire/catalog/index.blade.php b/resources/views/livewire/catalog/index.blade.php index 29738ab..71de121 100644 --- a/resources/views/livewire/catalog/index.blade.php +++ b/resources/views/livewire/catalog/index.blade.php @@ -6,7 +6,7 @@ use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Livewire\Attributes\Lazy; -use Livewire\Volt\Component; +use Livewire\Component; use Symfony\Component\Yaml\Yaml; new diff --git a/resources/views/livewire/catalog/trmnl.blade.php b/resources/views/livewire/catalog/trmnl.blade.php index cc8b070..5ab5224 100644 --- a/resources/views/livewire/catalog/trmnl.blade.php +++ b/resources/views/livewire/catalog/trmnl.blade.php @@ -5,7 +5,7 @@ use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Livewire\Attributes\Lazy; -use Livewire\Volt\Component; +use Livewire\Component; new #[Lazy] diff --git a/resources/views/livewire/codemirror.blade.php b/resources/views/livewire/codemirror.blade.php index fad3e53..acd8e2b 100644 --- a/resources/views/livewire/codemirror.blade.php +++ b/resources/views/livewire/codemirror.blade.php @@ -1,6 +1,6 @@ -
Loading editor...
- +
diff --git a/resources/views/livewire/device-dashboard.blade.php b/resources/views/livewire/device-dashboard.blade.php index 7fd48a8..703920c 100644 --- a/resources/views/livewire/device-dashboard.blade.php +++ b/resources/views/livewire/device-dashboard.blade.php @@ -1,6 +1,6 @@ user()->plugins->contains($this->plugin), 403); abort_unless($this->plugin->plugin_type === 'image_webhook', 404); - + $this->name = $this->plugin->name; } @@ -272,11 +272,11 @@ new class extends Component { copyable /> POST an image (PNG or BMP) to this URL to update the displayed image. - + Images must be posted in a format that can directly be read by the device. You need to take care of image format, dithering, and bit-depth. Check device logs if the image is not shown. - + diff --git a/resources/views/livewire/plugins/image-webhook.blade.php b/resources/views/livewire/plugins/image-webhook.blade.php index 3161443..788cbdb 100644 --- a/resources/views/livewire/plugins/image-webhook.blade.php +++ b/resources/views/livewire/plugins/image-webhook.blade.php @@ -1,7 +1,7 @@ user() !== null, 403); - + $plugin = Plugin::where('id', $pluginId) ->where('user_id', auth()->id()) ->where('plugin_type', 'image_webhook') diff --git a/resources/views/livewire/plugins/index.blade.php b/resources/views/livewire/plugins/index.blade.php index d902183..26a7a41 100644 --- a/resources/views/livewire/plugins/index.blade.php +++ b/resources/views/livewire/plugins/index.blade.php @@ -2,7 +2,7 @@ use App\Console\Commands\ExampleRecipesSeederCommand; use App\Services\PluginImportService; -use Livewire\Volt\Component; +use Livewire\Component; use Livewire\WithFileUploads; use Illuminate\Support\Str; diff --git a/resources/views/livewire/plugins/markup.blade.php b/resources/views/livewire/plugins/markup.blade.php index cb7823e..e78f137 100644 --- a/resources/views/livewire/plugins/markup.blade.php +++ b/resources/views/livewire/plugins/markup.blade.php @@ -2,7 +2,7 @@ use App\Jobs\GenerateScreenJob; use Illuminate\Support\Collection; -use Livewire\Volt\Component; +use Livewire\Component; new class extends Component { diff --git a/resources/views/livewire/plugins/recipe.blade.php b/resources/views/livewire/plugins/recipe.blade.php index 0e29e76..6e13dfb 100644 --- a/resources/views/livewire/plugins/recipe.blade.php +++ b/resources/views/livewire/plugins/recipe.blade.php @@ -5,7 +5,7 @@ use App\Models\Plugin; use App\Models\DeviceModel; use Illuminate\Support\Carbon; use Keepsuit\Liquid\Exceptions\LiquidException; -use Livewire\Volt\Component; +use Livewire\Component; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Http; diff --git a/resources/views/livewire/plugins/recipes/settings.blade.php b/resources/views/livewire/plugins/recipes/settings.blade.php index 8ae3d6f..e87ad78 100644 --- a/resources/views/livewire/plugins/recipes/settings.blade.php +++ b/resources/views/livewire/plugins/recipes/settings.blade.php @@ -2,7 +2,7 @@ use App\Models\Plugin; use Illuminate\Validation\Rule; -use Livewire\Volt\Component; +use Livewire\Component; /* * This component contains the TRMNL Plugin Settings modal diff --git a/resources/views/livewire/settings/appearance.blade.php b/resources/views/livewire/settings/appearance.blade.php index d485f7d..af056b0 100644 --- a/resources/views/livewire/settings/appearance.blade.php +++ b/resources/views/livewire/settings/appearance.blade.php @@ -1,6 +1,6 @@ +
@include('partials.settings-heading') diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php index 96fa464..7b4cba9 100644 --- a/resources/views/welcome.blade.php +++ b/resources/views/welcome.blade.php @@ -1,4 +1,4 @@ - +
@@ -50,7 +50,7 @@ return null; }); $latestVersion = Arr::get($response, 'tag_name'); - + if ($latestVersion && version_compare($latestVersion, config('app.version'), '>')) { $newVersion = $latestVersion; } @@ -67,4 +67,4 @@ @endif @endif @endauth -
+ diff --git a/routes/auth.php b/routes/auth.php index 49b2173..4f493e2 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -3,21 +3,20 @@ use App\Http\Controllers\Auth\OidcController; use App\Http\Controllers\Auth\VerifyEmailController; use Illuminate\Support\Facades\Route; -use Livewire\Volt\Volt; Route::middleware('guest')->group(function () { - Volt::route('login', 'auth.login') + Route::livewire('login', 'auth.login') ->name('login'); if (config('app.registration.enabled')) { - Volt::route('register', 'auth.register') + Route::livewire('register', 'auth.register') ->name('register'); } - Volt::route('forgot-password', 'auth.forgot-password') + Route::livewire('forgot-password', 'auth.forgot-password') ->name('password.request'); - Volt::route('reset-password/{token}', 'auth.reset-password') + Route::livewire('reset-password/{token}', 'auth.reset-password') ->name('password.reset'); // OIDC authentication routes @@ -30,14 +29,14 @@ Route::middleware('guest')->group(function () { }); Route::middleware('auth')->group(function () { - Volt::route('verify-email', 'auth.verify-email') + Route::livewire('verify-email', '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', 'auth.confirm-password') + Route::livewire('confirm-password', 'auth.confirm-password') ->name('password.confirm'); }); diff --git a/routes/web.php b/routes/web.php index b3069bd..d7007e4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,7 +3,6 @@ use App\Models\Plugin; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; -use Livewire\Volt\Volt; Route::get('/', function () { return view('welcome'); @@ -11,29 +10,29 @@ Route::get('/', function () { Route::middleware(['auth'])->group(function () { Route::redirect('settings', 'settings/preferences'); - Volt::route('settings/preferences', 'settings.preferences')->name('settings.preferences'); - Volt::route('settings/profile', 'settings.profile')->name('settings.profile'); - Volt::route('settings/password', 'settings.password')->name('settings.password'); - Volt::route('settings/appearance', 'settings.appearance')->name('settings.appearance'); - Volt::route('settings/support', 'settings.support')->name('settings.support'); + Route::livewire('settings/preferences', 'settings.preferences')->name('settings.preferences'); + Route::livewire('settings/profile', 'settings.profile')->name('settings.profile'); + Route::livewire('settings/password', 'settings.password')->name('settings.password'); + Route::livewire('settings/appearance', 'settings.appearance')->name('settings.appearance'); + Route::livewire('settings/support', 'settings.support')->name('settings.support'); - Volt::route('/dashboard', 'device-dashboard')->name('dashboard'); + Route::livewire('/dashboard', 'device-dashboard')->name('dashboard'); - Volt::route('/devices', 'devices.manage')->name('devices'); - Volt::route('/devices/{device}/configure', 'devices.configure')->name('devices.configure'); - Volt::route('/devices/{device}/logs', 'devices.logs')->name('devices.logs'); + Route::livewire('/devices', 'devices.manage')->name('devices'); + Route::livewire('/devices/{device}/configure', 'devices.configure')->name('devices.configure'); + Route::livewire('/devices/{device}/logs', 'devices.logs')->name('devices.logs'); - Volt::route('/device-models', 'device-models.index')->name('device-models.index'); - Volt::route('/device-palettes', 'device-palettes.index')->name('device-palettes.index'); + Route::livewire('/device-models', 'device-models.index')->name('device-models.index'); + Route::livewire('/device-palettes', 'device-palettes.index')->name('device-palettes.index'); - Volt::route('plugins', 'plugins.index')->name('plugins.index'); + Route::livewire('plugins', 'plugins.index')->name('plugins.index'); - Volt::route('plugins/recipe/{plugin}', 'plugins.recipe')->name('plugins.recipe'); - Volt::route('plugins/markup', 'plugins.markup')->name('plugins.markup'); - Volt::route('plugins/api', 'plugins.api')->name('plugins.api'); - Volt::route('plugins/image-webhook', 'plugins.image-webhook')->name('plugins.image-webhook'); - Volt::route('plugins/image-webhook/{plugin}', 'plugins.image-webhook-instance')->name('plugins.image-webhook-instance'); - Volt::route('playlists', 'playlists.index')->name('playlists.index'); + Route::livewire('plugins/recipe/{plugin}', 'plugins.recipe')->name('plugins.recipe'); + Route::livewire('plugins/markup', 'plugins.markup')->name('plugins.markup'); + Route::livewire('plugins/api', 'plugins.api')->name('plugins.api'); + Route::livewire('plugins/image-webhook', 'plugins.image-webhook')->name('plugins.image-webhook'); + Route::livewire('plugins/image-webhook/{plugin}', 'plugins.image-webhook-instance')->name('plugins.image-webhook-instance'); + Route::livewire('playlists', 'playlists.index')->name('playlists.index'); Route::get('plugin_settings/{trmnlp_id}/edit', function (Request $request, string $trmnlp_id) { $plugin = Plugin::query() diff --git a/tests/Feature/Auth/AuthenticationTest.php b/tests/Feature/Auth/AuthenticationTest.php index 07c1683..68c9648 100644 --- a/tests/Feature/Auth/AuthenticationTest.php +++ b/tests/Feature/Auth/AuthenticationTest.php @@ -1,7 +1,6 @@ create(); - $response = LivewireVolt::test('auth.login') + $response = Livewire::test('auth.login') ->set('email', $user->email) ->set('password', 'password') ->call('login'); diff --git a/tests/Feature/Auth/PasswordConfirmationTest.php b/tests/Feature/Auth/PasswordConfirmationTest.php index 265963a..6896206 100644 --- a/tests/Feature/Auth/PasswordConfirmationTest.php +++ b/tests/Feature/Auth/PasswordConfirmationTest.php @@ -1,7 +1,6 @@ actingAs($user); - $response = Volt::test('auth.confirm-password') + $response = Livewire::test('auth.confirm-password') ->set('password', 'password') ->call('confirmPassword'); @@ -32,7 +31,7 @@ test('password is not confirmed with invalid password', function (): void { $this->actingAs($user); - $response = Volt::test('auth.confirm-password') + $response = Livewire::test('auth.confirm-password') ->set('password', 'wrong-password') ->call('confirmPassword'); diff --git a/tests/Feature/Auth/PasswordResetTest.php b/tests/Feature/Auth/PasswordResetTest.php index 2f38263..b53f103 100644 --- a/tests/Feature/Auth/PasswordResetTest.php +++ b/tests/Feature/Auth/PasswordResetTest.php @@ -3,7 +3,6 @@ use App\Models\User; use Illuminate\Auth\Notifications\ResetPassword; use Illuminate\Support\Facades\Notification; -use Livewire\Volt\Volt; uses(Illuminate\Foundation\Testing\RefreshDatabase::class); @@ -18,7 +17,7 @@ test('reset password link can be requested', function (): void { $user = User::factory()->create(); - Volt::test('auth.forgot-password') + Livewire::test('auth.forgot-password') ->set('email', $user->email) ->call('sendPasswordResetLink'); @@ -30,7 +29,7 @@ test('reset password screen can be rendered', function (): void { $user = User::factory()->create(); - Volt::test('auth.forgot-password') + Livewire::test('auth.forgot-password') ->set('email', $user->email) ->call('sendPasswordResetLink'); @@ -48,12 +47,12 @@ test('password can be reset with valid token', function (): void { $user = User::factory()->create(); - Volt::test('auth.forgot-password') + Livewire::test('auth.forgot-password') ->set('email', $user->email) ->call('sendPasswordResetLink'); Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user): true { - $response = Volt::test('auth.reset-password', ['token' => $notification->token]) + $response = Livewire::test('auth.reset-password', ['token' => $notification->token]) ->set('email', $user->email) ->set('password', 'password') ->set('password_confirmation', 'password') diff --git a/tests/Feature/Auth/RegistrationTest.php b/tests/Feature/Auth/RegistrationTest.php index 45bc39b..2f931be 100644 --- a/tests/Feature/Auth/RegistrationTest.php +++ b/tests/Feature/Auth/RegistrationTest.php @@ -1,7 +1,5 @@ set('name', 'Test User') ->set('email', 'test@example.com') ->set('password', 'password') diff --git a/tests/Feature/Devices/ManageTest.php b/tests/Feature/Devices/ManageTest.php index fbfd2f2..0e9e594 100644 --- a/tests/Feature/Devices/ManageTest.php +++ b/tests/Feature/Devices/ManageTest.php @@ -2,7 +2,6 @@ use App\Models\Device; use App\Models\User; -use Livewire\Volt\Volt; uses(Illuminate\Foundation\Testing\RefreshDatabase::class); @@ -27,7 +26,7 @@ test('user can create a new device', function (): void { 'friendly_id' => 'test-device-1', ]; - $response = Volt::test('devices.manage') + $response = Livewire::test('devices.manage') ->set('name', $deviceData['name']) ->set('mac_address', $deviceData['mac_address']) ->set('api_key', $deviceData['api_key']) @@ -52,7 +51,7 @@ test('device creation requires required fields', function (): void { $user = User::factory()->create(); $this->actingAs($user); - $response = Volt::test('devices.manage') + $response = Livewire::test('devices.manage') ->set('name', '') ->set('mac_address', '') ->set('api_key', '') @@ -75,14 +74,14 @@ test('user can toggle proxy cloud for their device', function (): void { 'proxy_cloud' => false, ]); - $response = Volt::test('devices.manage') + $response = Livewire::test('devices.manage') ->call('toggleProxyCloud', $device); $response->assertHasNoErrors(); expect($device->fresh()->proxy_cloud)->toBeTrue(); // Toggle back to false - $response = Volt::test('devices.manage') + $response = Livewire::test('devices.manage') ->call('toggleProxyCloud', $device); expect($device->fresh()->proxy_cloud)->toBeFalse(); @@ -98,7 +97,7 @@ test('user cannot toggle proxy cloud for other users devices', function (): void 'proxy_cloud' => false, ]); - $response = Volt::test('devices.manage') + $response = Livewire::test('devices.manage') ->call('toggleProxyCloud', $device); $response->assertStatus(403); diff --git a/tests/Feature/Livewire/Catalog/IndexTest.php b/tests/Feature/Livewire/Catalog/IndexTest.php index 1b2efba..0551ccc 100644 --- a/tests/Feature/Livewire/Catalog/IndexTest.php +++ b/tests/Feature/Livewire/Catalog/IndexTest.php @@ -4,7 +4,6 @@ use App\Models\User; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; use Livewire\Livewire; -use Livewire\Volt\Volt; use Symfony\Component\Yaml\Yaml; beforeEach(function (): void { @@ -19,7 +18,7 @@ it('can render catalog component', function (): void { Livewire::withoutLazyLoading(); - $component = Volt::test('catalog.index'); + $component = Livewire::test('catalog.index'); $component->assertSee('No plugins available'); }); @@ -59,7 +58,7 @@ it('loads plugins from catalog URL', function (): void { Livewire::withoutLazyLoading(); - $component = Volt::test('catalog.index'); + $component = Livewire::test('catalog.index'); $component->assertSee('Test Plugin'); $component->assertSee('testuser'); @@ -102,7 +101,7 @@ it('hides preview button when screenshot_url is missing', function (): void { Livewire::withoutLazyLoading(); - Volt::test('catalog.index') + Livewire::test('catalog.index') ->assertSee('Test Plugin Without Screenshot') ->assertDontSeeHtml('variant="subtle" icon="eye"'); }); @@ -114,7 +113,7 @@ it('shows error when plugin not found', function (): void { Livewire::withoutLazyLoading(); - $component = Volt::test('catalog.index'); + $component = Livewire::test('catalog.index'); $component->call('installPlugin', 'non-existent-plugin'); @@ -146,7 +145,7 @@ it('shows error when zip_url is missing', function (): void { Livewire::withoutLazyLoading(); - $component = Volt::test('catalog.index'); + $component = Livewire::test('catalog.index'); $component->call('installPlugin', 'test-plugin'); @@ -189,7 +188,7 @@ it('can preview a plugin', function (): void { Livewire::withoutLazyLoading(); - Volt::test('catalog.index') + Livewire::test('catalog.index') ->assertSee('Test Plugin') ->call('previewPlugin', 'test-plugin') ->assertSet('previewingPlugin', 'test-plugin') diff --git a/tests/Feature/Livewire/Plugins/ConfigModalTest.php b/tests/Feature/Livewire/Plugins/ConfigModalTest.php index 4372991..0807d8e 100644 --- a/tests/Feature/Livewire/Plugins/ConfigModalTest.php +++ b/tests/Feature/Livewire/Plugins/ConfigModalTest.php @@ -3,7 +3,6 @@ use App\Models\Plugin; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; -use Livewire\Volt\Volt; use Illuminate\Support\Str; uses(RefreshDatabase::class); @@ -28,7 +27,7 @@ test('config modal correctly loads multi_string defaults into UI boxes', functio 'configuration' => ['tags' => 'alpha,beta'] ]); - Volt::test('plugins.config-modal', ['plugin' => $plugin]) + Livewire::test('plugins.config-modal', ['plugin' => $plugin]) ->assertSet('multiValues.tags', ['alpha', 'beta']); }); @@ -50,7 +49,7 @@ test('config modal validates against commas in multi_string boxes', function (): ] ]); - Volt::test('plugins.config-modal', ['plugin' => $plugin]) + Livewire::test('plugins.config-modal', ['plugin' => $plugin]) ->set('multiValues.tags.0', 'no,commas,allowed') ->call('saveConfiguration') ->assertHasErrors(['multiValues.tags.0' => 'regex']); @@ -78,7 +77,7 @@ test('config modal merges multi_string boxes into a single CSV string on save', 'configuration' => [] ]); - Volt::test('plugins.config-modal', ['plugin' => $plugin]) + Livewire::test('plugins.config-modal', ['plugin' => $plugin]) ->set('multiValues.items.0', 'First') ->call('addMultiItem', 'items') ->set('multiValues.items.1', 'Second') @@ -100,7 +99,7 @@ test('config modal resetForm clears dirty state and increments resetIndex', func 'configuration' => ['simple_key' => 'original_value'] ]); - Volt::test('plugins.config-modal', ['plugin' => $plugin]) + Livewire::test('plugins.config-modal', ['plugin' => $plugin]) ->set('configuration.simple_key', 'dirty_value') ->call('resetForm') ->assertSet('configuration.simple_key', 'original_value') @@ -118,7 +117,7 @@ test('config modal dispatches update event for parent warning refresh', function 'data_strategy' => 'static' ]); - Volt::test('plugins.config-modal', ['plugin' => $plugin]) + Livewire::test('plugins.config-modal', ['plugin' => $plugin]) ->call('saveConfiguration') ->assertDispatched('config-updated'); }); diff --git a/tests/Feature/Livewire/Plugins/RecipeSettingsTest.php b/tests/Feature/Livewire/Plugins/RecipeSettingsTest.php index a04815f..c625262 100644 --- a/tests/Feature/Livewire/Plugins/RecipeSettingsTest.php +++ b/tests/Feature/Livewire/Plugins/RecipeSettingsTest.php @@ -6,7 +6,6 @@ use App\Models\Plugin; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Str; -use Livewire\Volt\Volt; uses(RefreshDatabase::class); @@ -21,7 +20,7 @@ test('recipe settings can save trmnlp_id', function (): void { $trmnlpId = (string) Str::uuid(); - Volt::test('plugins.recipes.settings', ['plugin' => $plugin]) + Livewire::test('plugins.recipes.settings', ['plugin' => $plugin]) ->set('trmnlp_id', $trmnlpId) ->call('saveTrmnlpId') ->assertHasNoErrors(); @@ -43,7 +42,7 @@ test('recipe settings validates trmnlp_id is unique per user', function (): void 'trmnlp_id' => null, ]); - Volt::test('plugins.recipes.settings', ['plugin' => $newPlugin]) + Livewire::test('plugins.recipes.settings', ['plugin' => $newPlugin]) ->set('trmnlp_id', 'existing-id-123') ->call('saveTrmnlpId') ->assertHasErrors(['trmnlp_id' => 'unique']); @@ -67,7 +66,7 @@ test('recipe settings allows same trmnlp_id for different users', function (): v $this->actingAs($user2); - Volt::test('plugins.recipes.settings', ['plugin' => $plugin2]) + Livewire::test('plugins.recipes.settings', ['plugin' => $plugin2]) ->set('trmnlp_id', 'shared-id-123') ->call('saveTrmnlpId') ->assertHasNoErrors(); @@ -86,7 +85,7 @@ test('recipe settings allows same trmnlp_id for the same plugin', function (): v 'trmnlp_id' => $trmnlpId, ]); - Volt::test('plugins.recipes.settings', ['plugin' => $plugin]) + Livewire::test('plugins.recipes.settings', ['plugin' => $plugin]) ->set('trmnlp_id', $trmnlpId) ->call('saveTrmnlpId') ->assertHasNoErrors(); @@ -103,7 +102,7 @@ test('recipe settings can clear trmnlp_id', function (): void { 'trmnlp_id' => 'some-id', ]); - Volt::test('plugins.recipes.settings', ['plugin' => $plugin]) + Livewire::test('plugins.recipes.settings', ['plugin' => $plugin]) ->set('trmnlp_id', '') ->call('saveTrmnlpId') ->assertHasNoErrors(); diff --git a/tests/Feature/Settings/PasswordUpdateTest.php b/tests/Feature/Settings/PasswordUpdateTest.php index 0e33955..0c40594 100644 --- a/tests/Feature/Settings/PasswordUpdateTest.php +++ b/tests/Feature/Settings/PasswordUpdateTest.php @@ -2,7 +2,6 @@ use App\Models\User; use Illuminate\Support\Facades\Hash; -use Livewire\Volt\Volt; uses(Illuminate\Foundation\Testing\RefreshDatabase::class); @@ -13,7 +12,7 @@ test('password can be updated', function (): void { $this->actingAs($user); - $response = Volt::test('settings.password') + $response = Livewire::test('settings.password') ->set('current_password', 'password') ->set('password', 'new-password') ->set('password_confirmation', 'new-password') @@ -31,7 +30,7 @@ test('correct password must be provided to update password', function (): void { $this->actingAs($user); - $response = Volt::test('settings.password') + $response = Livewire::test('settings.password') ->set('current_password', 'wrong-password') ->set('password', 'new-password') ->set('password_confirmation', 'new-password') diff --git a/tests/Feature/Settings/ProfileUpdateTest.php b/tests/Feature/Settings/ProfileUpdateTest.php index cbf424c..8bb21af 100644 --- a/tests/Feature/Settings/ProfileUpdateTest.php +++ b/tests/Feature/Settings/ProfileUpdateTest.php @@ -1,7 +1,6 @@ actingAs($user); - $response = Volt::test('settings.profile') + $response = Livewire::test('settings.profile') ->set('name', 'Test User') ->set('email', 'test@example.com') ->call('updateProfileInformation'); @@ -35,7 +34,7 @@ test('email verification status is unchanged when email address is unchanged', f $this->actingAs($user); - $response = Volt::test('settings.profile') + $response = Livewire::test('settings.profile') ->set('name', 'Test User') ->set('email', $user->email) ->call('updateProfileInformation'); @@ -50,7 +49,7 @@ test('user can delete their account', function (): void { $this->actingAs($user); - $response = Volt::test('settings.delete-user-form') + $response = Livewire::test('settings.delete-user-form') ->set('password', 'password') ->call('deleteUser'); @@ -67,7 +66,7 @@ test('correct password must be provided to delete account', function (): void { $this->actingAs($user); - $response = Volt::test('settings.delete-user-form') + $response = Livewire::test('settings.delete-user-form') ->set('password', 'wrong-password') ->call('deleteUser'); diff --git a/tests/Feature/Volt/CatalogTrmnlTest.php b/tests/Feature/Volt/CatalogTrmnlTest.php index a80c63a..536f6ad 100644 --- a/tests/Feature/Volt/CatalogTrmnlTest.php +++ b/tests/Feature/Volt/CatalogTrmnlTest.php @@ -5,7 +5,6 @@ declare(strict_types=1); use App\Models\User; use Illuminate\Support\Facades\Http; use Livewire\Livewire; -use Livewire\Volt\Volt; it('loads newest TRMNL recipes on mount', function (): void { Http::fake([ @@ -25,7 +24,7 @@ it('loads newest TRMNL recipes on mount', function (): void { Livewire::withoutLazyLoading(); - Volt::test('catalog.trmnl') + Livewire::test('catalog.trmnl') ->assertSee('Weather Chum') ->assertSee('Install') ->assertDontSeeHtml('variant="subtle" icon="eye"') @@ -50,7 +49,7 @@ it('shows preview button when screenshot_url is provided', function (): void { Livewire::withoutLazyLoading(); - Volt::test('catalog.trmnl') + Livewire::test('catalog.trmnl') ->assertSee('Weather Chum') ->assertSee('Preview'); }); @@ -88,7 +87,7 @@ it('searches TRMNL recipes when search term is provided', function (): void { Livewire::withoutLazyLoading(); - Volt::test('catalog.trmnl') + Livewire::test('catalog.trmnl') ->assertSee('Initial Recipe') ->set('search', 'weather') ->assertSee('Weather Search Result') @@ -118,7 +117,7 @@ it('installs plugin successfully when user is authenticated', function (): void Livewire::withoutLazyLoading(); - Volt::test('catalog.trmnl') + Livewire::test('catalog.trmnl') ->assertSee('Weather Chum') ->call('installPlugin', '123') ->assertSee('Error installing plugin'); // This will fail because we don't have a real zip file @@ -142,7 +141,7 @@ it('shows error when user is not authenticated', function (): void { Livewire::withoutLazyLoading(); - Volt::test('catalog.trmnl') + Livewire::test('catalog.trmnl') ->assertSee('Weather Chum') ->call('installPlugin', '123') ->assertStatus(403); // This will return 403 because user is not authenticated @@ -171,7 +170,7 @@ it('shows error when plugin installation fails', function (): void { Livewire::withoutLazyLoading(); - Volt::test('catalog.trmnl') + Livewire::test('catalog.trmnl') ->assertSee('Weather Chum') ->call('installPlugin', '123') ->assertSee('Error installing plugin'); // This will fail because the zip content is invalid @@ -205,7 +204,7 @@ it('previews a recipe with async fetch', function (): void { Livewire::withoutLazyLoading(); - Volt::test('catalog.trmnl') + Livewire::test('catalog.trmnl') ->assertSee('Weather Chum') ->call('previewRecipe', '123') ->assertSet('previewingRecipe', '123') @@ -247,7 +246,7 @@ it('supports pagination and loading more recipes', function (): void { Livewire::withoutLazyLoading(); - Volt::test('catalog.trmnl') + Livewire::test('catalog.trmnl') ->assertSee('Recipe Page 1') ->assertDontSee('Recipe Page 2') ->assertSee('Load next page') @@ -276,7 +275,7 @@ it('resets pagination when search term changes', function (): void { Livewire::withoutLazyLoading(); - Volt::test('catalog.trmnl') + Livewire::test('catalog.trmnl') ->assertSee('Initial 1') ->call('loadMore') ->set('search', 'weather') diff --git a/tests/Feature/Volt/DevicePalettesTest.php b/tests/Feature/Volt/DevicePalettesTest.php index 376a4a6..f94708e 100644 --- a/tests/Feature/Volt/DevicePalettesTest.php +++ b/tests/Feature/Volt/DevicePalettesTest.php @@ -4,7 +4,6 @@ declare(strict_types=1); use App\Models\DevicePalette; use App\Models\User; -use Livewire\Volt\Volt; uses(Illuminate\Foundation\Testing\RefreshDatabase::class); @@ -25,7 +24,7 @@ test('component loads all device palettes on mount', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index'); + $component = Livewire::test('device-palettes.index'); $palettes = $component->get('devicePalettes'); expect($palettes)->toHaveCount($initialCount + 3); @@ -36,7 +35,7 @@ test('can open modal to create new device palette', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->call('openDevicePaletteModal'); $component @@ -51,7 +50,7 @@ test('can create a new device palette', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->set('name', 'test-palette') ->set('description', 'Test Palette Description') ->set('grays', 16) @@ -76,7 +75,7 @@ test('can create a grayscale-only palette without colors', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->set('name', 'grayscale-palette') ->set('grays', 256) ->set('colors', []) @@ -102,7 +101,7 @@ test('can open modal to edit existing device palette', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->call('openDevicePaletteModal', $palette->id); $component @@ -125,7 +124,7 @@ test('can update an existing device palette', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->call('openDevicePaletteModal', $palette->id) ->set('name', 'updated-palette') ->set('description', 'Updated Description') @@ -153,7 +152,7 @@ test('can delete a device palette', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->call('deleteDevicePalette', $palette->id); expect(DevicePalette::find($palette->id))->toBeNull(); @@ -175,7 +174,7 @@ test('can duplicate a device palette', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->call('duplicateDevicePalette', $palette->id); $component @@ -192,7 +191,7 @@ test('can add a color to the colors array', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->set('colorInput', '#FF0000') ->call('addColor'); @@ -207,7 +206,7 @@ test('cannot add duplicate colors', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->set('colors', ['#FF0000']) ->set('colorInput', '#FF0000') ->call('addColor'); @@ -222,7 +221,7 @@ test('can add multiple colors', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->set('colorInput', '#FF0000') ->call('addColor') ->set('colorInput', '#00FF00') @@ -239,7 +238,7 @@ test('can remove a color from the colors array', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->set('colors', ['#FF0000', '#00FF00', '#0000FF']) ->call('removeColor', 1); @@ -251,7 +250,7 @@ test('removing color reindexes array', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->set('colors', ['#FF0000', '#00FF00', '#0000FF']) ->call('removeColor', 0); @@ -271,7 +270,7 @@ test('can open modal in view-only mode for api-sourced palette', function (): vo $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->call('openDevicePaletteModal', $palette->id, true); $component @@ -284,7 +283,7 @@ test('name is required when creating device palette', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->set('grays', 16) ->call('saveDevicePalette'); @@ -301,7 +300,7 @@ test('name must be unique when creating device palette', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->set('name', 'existing-name') ->set('grays', 16) ->call('saveDevicePalette'); @@ -320,7 +319,7 @@ test('name can be same when updating device palette', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->call('openDevicePaletteModal', $palette->id) ->set('grays', 16) ->call('saveDevicePalette'); @@ -333,7 +332,7 @@ test('grays is required when creating device palette', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->set('name', 'test-palette') ->set('grays', null) ->call('saveDevicePalette'); @@ -346,7 +345,7 @@ test('grays must be at least 1', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->set('name', 'test-palette') ->set('grays', 0) ->call('saveDevicePalette'); @@ -359,7 +358,7 @@ test('grays must be at most 256', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->set('name', 'test-palette') ->set('grays', 257) ->call('saveDevicePalette'); @@ -372,7 +371,7 @@ test('colors must be valid hex format', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->set('name', 'test-palette') ->set('grays', 16) ->set('colors', ['invalid-color', '#FF0000']) @@ -386,7 +385,7 @@ test('color input must be valid hex format when adding color', function (): void $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->set('colorInput', 'invalid-color') ->call('addColor'); @@ -398,7 +397,7 @@ test('color input accepts valid hex format', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->set('colorInput', '#FF0000') ->call('addColor'); @@ -410,7 +409,7 @@ test('color input accepts lowercase hex format', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->set('colorInput', '#ff0000') ->call('addColor'); @@ -422,7 +421,7 @@ test('description can be null', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->set('name', 'test-palette') ->set('grays', 16) ->set('description', null) @@ -439,7 +438,7 @@ test('framework class can be empty string', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->set('name', 'test-palette') ->set('grays', 16) ->set('framework_class', '') @@ -456,7 +455,7 @@ test('empty colors array is saved as null', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->set('name', 'test-palette') ->set('grays', 16) ->set('colors', []) @@ -473,7 +472,7 @@ test('component resets form after saving', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->set('name', 'test-palette') ->set('description', 'Test Description') ->set('grays', 16) @@ -503,7 +502,7 @@ test('component handles palette with null colors when editing', function (): voi $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->call('openDevicePaletteModal', $palette->id); $component->assertSet('colors', []); @@ -524,7 +523,7 @@ test('component handles palette with string colors when editing', function (): v $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->call('openDevicePaletteModal', $palette->id); $component->assertSet('colors', ['#FF0000', '#00FF00']); @@ -538,7 +537,7 @@ test('component refreshes palette list after creating', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->set('name', 'new-palette') ->set('grays', 16) ->call('saveDevicePalette'); @@ -566,7 +565,7 @@ test('component refreshes palette list after deleting', function (): void { $this->actingAs($user); - $component = Volt::test('device-palettes.index') + $component = Livewire::test('device-palettes.index') ->call('deleteDevicePalette', $palette1->id); $palettes = $component->get('devicePalettes'); From d19a079b8a2593cd5630826ca63570a18e17468e Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Thu, 15 Jan 2026 09:01:59 +0100 Subject: [PATCH 2/7] chore: pint --- resources/views/flux/navlist/group.blade.php | 8 +- .../livewire/auth/confirm-password.blade.php | 3 +- .../livewire/auth/forgot-password.blade.php | 3 +- resources/views/livewire/auth/login.blade.php | 9 +- .../views/livewire/auth/register.blade.php | 8 +- .../livewire/auth/reset-password.blade.php | 8 +- .../livewire/auth/verify-email.blade.php | 3 +- .../views/livewire/catalog/trmnl.blade.php | 2 +- resources/views/livewire/codemirror.blade.php | 9 +- .../views/livewire/device-dashboard.blade.php | 3 +- .../livewire/devices/configure.blade.php | 50 +++++-- .../views/livewire/devices/logs.blade.php | 5 +- .../views/livewire/devices/manage.blade.php | 18 ++- .../views/livewire/playlists/index.blade.php | 22 +-- .../views/livewire/plugins/api.blade.php | 7 +- .../livewire/plugins/config-modal.blade.php | 25 ++-- .../plugins/image-webhook-instance.blade.php | 36 +++-- .../livewire/plugins/image-webhook.blade.php | 6 +- .../views/livewire/plugins/index.blade.php | 44 +++--- .../views/livewire/plugins/markup.blade.php | 23 ++-- .../views/livewire/plugins/recipe.blade.php | 128 +++++++++++------- .../plugins/recipes/settings.blade.php | 12 +- .../livewire/settings/appearance.blade.php | 3 +- .../settings/delete-user-form.blade.php | 3 +- .../livewire/settings/password.blade.php | 5 +- .../livewire/settings/preferences.blade.php | 7 +- .../views/livewire/settings/profile.blade.php | 6 +- .../views/livewire/settings/support.blade.php | 5 +- tests/Feature/Api/ImageWebhookTest.php | 6 +- .../Livewire/Plugins/ConfigModalTest.php | 16 +-- 30 files changed, 295 insertions(+), 188 deletions(-) diff --git a/resources/views/flux/navlist/group.blade.php b/resources/views/flux/navlist/group.blade.php index cecbabe..844171f 100644 --- a/resources/views/flux/navlist/group.blade.php +++ b/resources/views/flux/navlist/group.blade.php @@ -4,7 +4,7 @@ 'heading' => null, ]) - + class('group/disclosure') }} @@ -30,7 +30,7 @@ - +
class('block space-y-[2px]') }}>
@@ -42,10 +42,10 @@
- +
class('block space-y-[2px]') }}> {{ $slot }}
- + diff --git a/resources/views/livewire/auth/confirm-password.blade.php b/resources/views/livewire/auth/confirm-password.blade.php index faa24e5..e1b7aea 100644 --- a/resources/views/livewire/auth/confirm-password.blade.php +++ b/resources/views/livewire/auth/confirm-password.blade.php @@ -5,7 +5,8 @@ use Illuminate\Validation\ValidationException; use Livewire\Attributes\Layout; use Livewire\Component; -new #[Layout('layouts.auth')] class extends Component { +new #[Layout('layouts.auth')] class extends Component +{ public string $password = ''; /** diff --git a/resources/views/livewire/auth/forgot-password.blade.php b/resources/views/livewire/auth/forgot-password.blade.php index 158c3fc..a47f879 100644 --- a/resources/views/livewire/auth/forgot-password.blade.php +++ b/resources/views/livewire/auth/forgot-password.blade.php @@ -4,7 +4,8 @@ use Illuminate\Support\Facades\Password; use Livewire\Attributes\Layout; use Livewire\Component; -new #[Layout('layouts.auth')] class extends Component { +new #[Layout('layouts.auth')] class extends Component +{ public string $email = ''; /** diff --git a/resources/views/livewire/auth/login.blade.php b/resources/views/livewire/auth/login.blade.php index 645b866..65396d4 100644 --- a/resources/views/livewire/auth/login.blade.php +++ b/resources/views/livewire/auth/login.blade.php @@ -10,7 +10,8 @@ use Livewire\Attributes\Layout; use Livewire\Attributes\Validate; use Livewire\Component; -new #[Layout('layouts.auth')] class extends Component { +new #[Layout('layouts.auth')] class extends Component +{ #[Validate('required|string|email')] public string $email = ''; @@ -28,7 +29,7 @@ new #[Layout('layouts.auth')] class extends Component { $this->ensureIsNotRateLimited(); - if (!Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) { + if (! Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) { RateLimiter::hit($this->throttleKey()); throw ValidationException::withMessages([ @@ -47,7 +48,7 @@ new #[Layout('layouts.auth')] class extends Component { */ protected function ensureIsNotRateLimited(): void { - if (!RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { + if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { return; } @@ -68,7 +69,7 @@ new #[Layout('layouts.auth')] class extends Component { */ protected function throttleKey(): string { - return Str::transliterate(Str::lower($this->email) . '|' . request()->ip()); + return Str::transliterate(Str::lower($this->email).'|'.request()->ip()); } public function mount(): void diff --git a/resources/views/livewire/auth/register.blade.php b/resources/views/livewire/auth/register.blade.php index e98a4e1..8149fab 100644 --- a/resources/views/livewire/auth/register.blade.php +++ b/resources/views/livewire/auth/register.blade.php @@ -8,10 +8,14 @@ use Illuminate\Validation\Rules; use Livewire\Attributes\Layout; use Livewire\Component; -new #[Layout('layouts.auth')] class extends Component { +new #[Layout('layouts.auth')] class extends Component +{ public string $name = ''; + public string $email = ''; + public string $password = ''; + public string $password_confirmation = ''; /** @@ -21,7 +25,7 @@ new #[Layout('layouts.auth')] class extends Component { { $validated = $this->validate([ 'name' => ['required', 'string', 'max:255'], - 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:' . User::class], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], 'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()], ]); diff --git a/resources/views/livewire/auth/reset-password.blade.php b/resources/views/livewire/auth/reset-password.blade.php index a58fd31..0fd18d7 100644 --- a/resources/views/livewire/auth/reset-password.blade.php +++ b/resources/views/livewire/auth/reset-password.blade.php @@ -10,11 +10,15 @@ use Livewire\Attributes\Layout; use Livewire\Attributes\Locked; use Livewire\Component; -new #[Layout('layouts.auth')] class extends Component { +new #[Layout('layouts.auth')] class extends Component +{ #[Locked] public string $token = ''; + public string $email = ''; + public string $password = ''; + public string $password_confirmation = ''; /** @@ -56,7 +60,7 @@ new #[Layout('layouts.auth')] class extends Component { // 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::PasswordReset) { + if ($status !== Password::PasswordReset) { $this->addError('email', __($status)); return; diff --git a/resources/views/livewire/auth/verify-email.blade.php b/resources/views/livewire/auth/verify-email.blade.php index c05e3c4..c52e38c 100644 --- a/resources/views/livewire/auth/verify-email.blade.php +++ b/resources/views/livewire/auth/verify-email.blade.php @@ -6,7 +6,8 @@ use Illuminate\Support\Facades\Session; use Livewire\Attributes\Layout; use Livewire\Component; -new #[Layout('layouts.auth')] class extends Component { +new #[Layout('layouts.auth')] class extends Component +{ /** * Send an email verification notification to the user. */ diff --git a/resources/views/livewire/catalog/trmnl.blade.php b/resources/views/livewire/catalog/trmnl.blade.php index 5ab5224..0285669 100644 --- a/resources/views/livewire/catalog/trmnl.blade.php +++ b/resources/views/livewire/catalog/trmnl.blade.php @@ -122,7 +122,7 @@ class extends Component public function loadMore(): void { - $this->page++; + ++$this->page; $term = mb_trim($this->search); if ($term === '' || mb_strlen($term) < 2) { diff --git a/resources/views/livewire/codemirror.blade.php b/resources/views/livewire/codemirror.blade.php index acd8e2b..5b18d2a 100644 --- a/resources/views/livewire/codemirror.blade.php +++ b/resources/views/livewire/codemirror.blade.php @@ -1,17 +1,23 @@ id = $id; } - public function toJSON() { return json_encode([ diff --git a/resources/views/livewire/device-dashboard.blade.php b/resources/views/livewire/device-dashboard.blade.php index 703920c..1c9149f 100644 --- a/resources/views/livewire/device-dashboard.blade.php +++ b/resources/views/livewire/device-dashboard.blade.php @@ -2,7 +2,8 @@ use Livewire\Component; -new class extends Component { +new class extends Component +{ public function mount() { return view('livewire.device-dashboard', ['devices' => auth()->user()->devices()->paginate(10)]); diff --git a/resources/views/livewire/devices/configure.blade.php b/resources/views/livewire/devices/configure.blade.php index ffd315c..ce3e821 100644 --- a/resources/views/livewire/devices/configure.blade.php +++ b/resources/views/livewire/devices/configure.blade.php @@ -7,33 +7,50 @@ use App\Models\Playlist; use App\Models\PlaylistItem; use Livewire\Component; -new class extends Component { - +new class extends Component +{ public $device; public $name; + public $api_key; + public $friendly_id; + public $mac_address; + public $default_refresh_interval; + public $width; + public $height; + public $rotate; + public $image_format; + public $device_model_id; // Sleep mode and special function public $sleep_mode_enabled = false; + public $sleep_mode_from; + public $sleep_mode_to; + public $special_function; // Playlist properties public $playlists; + public $playlist_name; + public $selected_weekdays = null; + public $active_from; + public $active_until; + public $refresh_time = null; // Device model properties @@ -41,15 +58,17 @@ new class extends Component { // Firmware properties public $firmwares; + public $selected_firmware_id; + public $download_firmware; - public function mount(\App\Models\Device $device) + public function mount(App\Models\Device $device) { abort_unless(auth()->user()->devices->contains($device), 403); $current_image_uuid = $device->current_screen_image; - $current_image_path = 'images/generated/' . $current_image_uuid . '.png'; + $current_image_path = 'images/generated/'.$current_image_uuid.'.png'; $this->device = $device; $this->name = $device->name; @@ -65,10 +84,11 @@ new class extends Component { $this->deviceModels = DeviceModel::orderBy('label')->get()->sortBy(function ($deviceModel) { // Put TRMNL models at the top, then sort alphabetically within each group $isTrmnl = str_starts_with($deviceModel->label, 'TRMNL'); - return $isTrmnl ? '0' . $deviceModel->label : '1' . $deviceModel->label; + + return $isTrmnl ? '0'.$deviceModel->label : '1'.$deviceModel->label; }); $this->playlists = $device->playlists()->with('items.plugin')->orderBy('created_at')->get(); - $this->firmwares = \App\Models\Firmware::orderBy('latest', 'desc')->orderBy('created_at', 'desc')->get(); + $this->firmwares = Firmware::orderBy('latest', 'desc')->orderBy('created_at', 'desc')->get(); $this->selected_firmware_id = $this->firmwares->where('latest', true)->first()?->id; $this->sleep_mode_enabled = $device->sleep_mode_enabled ?? false; $this->sleep_mode_from = optional($device->sleep_mode_from)->format('H:i'); @@ -80,7 +100,7 @@ new class extends Component { ]); } - public function deleteDevice(\App\Models\Device $device) + public function deleteDevice(App\Models\Device $device) { abort_unless(auth()->user()->devices->contains($device), 403); $device->delete(); @@ -93,6 +113,7 @@ new class extends Component { // Convert empty string to null for custom selection if (empty($this->device_model_id)) { $this->device_model_id = null; + return; } @@ -162,7 +183,7 @@ new class extends Component { $this->refresh_time = null; } - if (empty($this->selected_weekdays)){ + if (empty($this->selected_weekdays)) { $this->selected_weekdays = null; } @@ -182,7 +203,7 @@ new class extends Component { public function togglePlaylistActive(Playlist $playlist) { - $playlist->update(['is_active' => !$playlist->is_active]); + $playlist->update(['is_active' => ! $playlist->is_active]); $this->playlists = $this->device->playlists()->with('items.plugin')->orderBy('created_at')->get(); } @@ -218,7 +239,7 @@ new class extends Component { public function togglePlaylistItemActive(PlaylistItem $item) { - $item->update(['is_active' => !$item->is_active]); + $item->update(['is_active' => ! $item->is_active]); $this->playlists = $this->device->playlists()->with('items.plugin')->orderBy('created_at')->get(); } @@ -227,7 +248,7 @@ new class extends Component { abort_unless(auth()->user()->devices->contains($playlist->device), 403); $playlist->delete(); $this->playlists = $this->device->playlists()->with('items.plugin')->orderBy('created_at')->get(); - Flux::modal('delete-playlist-' . $playlist->id)->close(); + Flux::modal('delete-playlist-'.$playlist->id)->close(); } public function deletePlaylistItem(PlaylistItem $item) @@ -235,7 +256,7 @@ new class extends Component { abort_unless(auth()->user()->devices->contains($item->playlist->device), 403); $item->delete(); $this->playlists = $this->device->playlists()->with('items.plugin')->orderBy('created_at')->get(); - Flux::modal('delete-playlist-item-' . $item->id)->close(); + Flux::modal('delete-playlist-item-'.$item->id)->close(); } public function editPlaylist(Playlist $playlist) @@ -258,7 +279,7 @@ new class extends Component { $this->refresh_time = null; } - if (empty($this->selected_weekdays)){ + if (empty($this->selected_weekdays)) { $this->selected_weekdays = null; } @@ -272,7 +293,7 @@ new class extends Component { $this->playlists = $this->device->playlists()->with('items.plugin')->orderBy('created_at')->get(); $this->reset(['playlist_name', 'selected_weekdays', 'active_from', 'active_until', 'refresh_time']); - Flux::modal('edit-playlist-' . $playlist->id)->close(); + Flux::modal('edit-playlist-'.$playlist->id)->close(); } public function preparePlaylistEdit(Playlist $playlist) @@ -292,7 +313,6 @@ new class extends Component { 'selected_firmware_id' => 'required|exists:firmware,id', ]); - if ($this->download_firmware) { FirmwareDownloadJob::dispatchSync(Firmware::find($this->selected_firmware_id)); } diff --git a/resources/views/livewire/devices/logs.blade.php b/resources/views/livewire/devices/logs.blade.php index 85f8653..6a8733c 100644 --- a/resources/views/livewire/devices/logs.blade.php +++ b/resources/views/livewire/devices/logs.blade.php @@ -1,11 +1,12 @@ deviceModels = DeviceModel::orderBy('label')->get()->sortBy(function ($deviceModel) { // Put TRMNL models at the top, then sort alphabetically within each group $isTrmnl = str_starts_with($deviceModel->label, 'TRMNL'); - return $isTrmnl ? '0' . $deviceModel->label : '1' . $deviceModel->label; + + return $isTrmnl ? '0'.$deviceModel->label : '1'.$deviceModel->label; }); + return view('livewire.devices.manage'); } @@ -81,7 +85,7 @@ new class extends Component { ]); $this->reset(); - \Flux::modal('create-device')->close(); + Flux::modal('create-device')->close(); $this->devices = auth()->user()->devices; session()->flash('message', 'Device created successfully.'); @@ -91,7 +95,7 @@ new class extends Component { { abort_unless(auth()->user()->devices->contains($device), 403); $device->update([ - 'proxy_cloud' => !$device->proxy_cloud, + 'proxy_cloud' => ! $device->proxy_cloud, ]); // if ($device->proxy_cloud) { @@ -108,9 +112,9 @@ new class extends Component { $pauseUntil = now()->addMinutes($this->pause_duration); $device->update(['pause_until' => $pauseUntil]); $this->reset('pause_duration'); - \Flux::modal('pause-device-' . $deviceId)->close(); + Flux::modal('pause-device-'.$deviceId)->close(); $this->devices = auth()->user()->devices; - session()->flash('message', 'Device paused until ' . $pauseUntil->format('H:i')); + session()->flash('message', 'Device paused until '.$pauseUntil->format('H:i')); } } diff --git a/resources/views/livewire/playlists/index.blade.php b/resources/views/livewire/playlists/index.blade.php index 5218b85..e6c6719 100644 --- a/resources/views/livewire/playlists/index.blade.php +++ b/resources/views/livewire/playlists/index.blade.php @@ -1,31 +1,37 @@ devices = auth()->user()->devices()->with(['playlists.items.plugin'])->get(); + return view('livewire.playlists.index'); } public function togglePlaylistActive(Playlist $playlist) { abort_unless(auth()->user()->devices->contains($playlist->device), 403); - $playlist->update(['is_active' => !$playlist->is_active]); + $playlist->update(['is_active' => ! $playlist->is_active]); $this->devices = auth()->user()->devices()->with(['playlists.items.plugin'])->get(); } @@ -64,7 +70,7 @@ new class extends Component { public function togglePlaylistItemActive(PlaylistItem $item) { abort_unless(auth()->user()->devices->contains($item->playlist->device), 403); - $item->update(['is_active' => !$item->is_active]); + $item->update(['is_active' => ! $item->is_active]); $this->devices = auth()->user()->devices()->with(['playlists.items.plugin'])->get(); } @@ -73,7 +79,7 @@ new class extends Component { abort_unless(auth()->user()->devices->contains($playlist->device), 403); $playlist->delete(); $this->devices = auth()->user()->devices()->with(['playlists.items.plugin'])->get(); - Flux::modal('delete-playlist-' . $playlist->id)->close(); + Flux::modal('delete-playlist-'.$playlist->id)->close(); } public function deletePlaylistItem(PlaylistItem $item) @@ -81,7 +87,7 @@ new class extends Component { abort_unless(auth()->user()->devices->contains($item->playlist->device), 403); $item->delete(); $this->devices = auth()->user()->devices()->with(['playlists.items.plugin'])->get(); - Flux::modal('delete-playlist-item-' . $item->id)->close(); + Flux::modal('delete-playlist-item-'.$item->id)->close(); } public function editPlaylist(Playlist $playlist) @@ -106,7 +112,7 @@ new class extends Component { $this->refresh_time = null; } - if (empty($this->selected_weekdays)){ + if (empty($this->selected_weekdays)) { $this->selected_weekdays = null; } @@ -120,7 +126,7 @@ new class extends Component { $this->devices = auth()->user()->devices()->with(['playlists.items.plugin'])->get(); $this->reset(['playlist_name', 'selected_weekdays', 'active_from', 'active_until', 'refresh_time']); - Flux::modal('edit-playlist-' . $playlist->id)->close(); + Flux::modal('edit-playlist-'.$playlist->id)->close(); } public function preparePlaylistEdit(Playlist $playlist) diff --git a/resources/views/livewire/plugins/api.blade.php b/resources/views/livewire/plugins/api.blade.php index 9b95490..56ac708 100644 --- a/resources/views/livewire/plugins/api.blade.php +++ b/resources/views/livewire/plugins/api.blade.php @@ -1,12 +1,13 @@ loadData(); + $this->loadData(); } public function loadData(): void @@ -38,7 +42,7 @@ new class extends Component { $fieldKey = $field['keyname']; $rawValue = $this->configuration[$fieldKey] ?? ($field['default'] ?? ''); - $currentValue = is_array($rawValue) ? '' : (string)$rawValue; + $currentValue = is_array($rawValue) ? '' : (string) $rawValue; $this->multiValues[$fieldKey] = $currentValue !== '' ? array_values(array_filter(explode(',', $currentValue))) @@ -51,10 +55,11 @@ new class extends Component { * Triggered by @close on the modal to discard any typed but unsaved changes */ public int $resetIndex = 0; // Add this property + public function resetForm(): void { $this->loadData(); - $this->resetIndex++; // Increment to force DOM refresh + ++$this->resetIndex; // Increment to force DOM refresh } public function saveConfiguration() @@ -131,7 +136,7 @@ new class extends Component { if ($query !== null) { $requestData = [ 'function' => $fieldKey, - 'query' => $query + 'query' => $query, ]; } @@ -144,7 +149,7 @@ new class extends Component { } else { $this->xhrSelectOptions[$fieldKey] = []; } - } catch (\Exception $e) { + } catch (Exception $e) { $this->xhrSelectOptions[$fieldKey] = []; } } @@ -152,11 +157,11 @@ new class extends Component { public function searchXhrSelect(string $fieldKey, string $endpoint): void { $query = $this->searchQueries[$fieldKey] ?? ''; - if (!empty($query)) { + if (! empty($query)) { $this->loadXhrSelectOptions($fieldKey, $endpoint, $query); } } -};?> +}; ?>
diff --git a/resources/views/livewire/plugins/image-webhook-instance.blade.php b/resources/views/livewire/plugins/image-webhook-instance.blade.php index e73b1ac..7822a2b 100644 --- a/resources/views/livewire/plugins/image-webhook-instance.blade.php +++ b/resources/views/livewire/plugins/image-webhook-instance.blade.php @@ -3,14 +3,22 @@ use App\Models\Plugin; use Livewire\Component; -new class extends Component { +new class extends Component +{ public Plugin $plugin; + public string $name; + public array $checked_devices = []; + public array $device_playlists = []; + public array $device_playlist_names = []; + public array $device_weekdays = []; + public array $device_active_from = []; + public array $device_active_until = []; public function mount(): void @@ -38,7 +46,6 @@ new class extends Component { $this->plugin->update(['name' => $this->name]); } - public function addToPlaylist() { $this->validate([ @@ -46,14 +53,16 @@ new class extends Component { ]); foreach ($this->checked_devices as $deviceId) { - if (!isset($this->device_playlists[$deviceId]) || empty($this->device_playlists[$deviceId])) { - $this->addError('device_playlists.' . $deviceId, 'Please select a playlist for each device.'); + if (! isset($this->device_playlists[$deviceId]) || empty($this->device_playlists[$deviceId])) { + $this->addError('device_playlists.'.$deviceId, 'Please select a playlist for each device.'); + return; } if ($this->device_playlists[$deviceId] === 'new') { - if (!isset($this->device_playlist_names[$deviceId]) || empty($this->device_playlist_names[$deviceId])) { - $this->addError('device_playlist_names.' . $deviceId, 'Playlist name is required when creating a new playlist.'); + if (! isset($this->device_playlist_names[$deviceId]) || empty($this->device_playlist_names[$deviceId])) { + $this->addError('device_playlist_names.'.$deviceId, 'Playlist name is required when creating a new playlist.'); + return; } } @@ -63,15 +72,15 @@ new class extends Component { $playlist = null; if ($this->device_playlists[$deviceId] === 'new') { - $playlist = \App\Models\Playlist::create([ + $playlist = App\Models\Playlist::create([ 'device_id' => $deviceId, 'name' => $this->device_playlist_names[$deviceId], - 'weekdays' => !empty($this->device_weekdays[$deviceId] ?? null) ? $this->device_weekdays[$deviceId] : null, + 'weekdays' => ! empty($this->device_weekdays[$deviceId] ?? null) ? $this->device_weekdays[$deviceId] : null, 'active_from' => $this->device_active_from[$deviceId] ?? null, 'active_until' => $this->device_active_until[$deviceId] ?? null, ]); } else { - $playlist = \App\Models\Playlist::findOrFail($this->device_playlists[$deviceId]); + $playlist = App\Models\Playlist::findOrFail($this->device_playlists[$deviceId]); } $maxOrder = $playlist->items()->max('order') ?? 0; @@ -96,16 +105,17 @@ new class extends Component { public function getDevicePlaylists($deviceId) { - return \App\Models\Playlist::where('device_id', $deviceId)->get(); + return App\Models\Playlist::where('device_id', $deviceId)->get(); } public function hasAnyPlaylistSelected(): bool { foreach ($this->checked_devices as $deviceId) { - if (isset($this->device_playlists[$deviceId]) && !empty($this->device_playlists[$deviceId])) { + if (isset($this->device_playlists[$deviceId]) && ! empty($this->device_playlists[$deviceId])) { return true; } } + return false; } @@ -118,14 +128,14 @@ new class extends Component { public function getImagePath(): ?string { - if (!$this->plugin->current_image) { + if (! $this->plugin->current_image) { return null; } $extensions = ['png', 'bmp']; foreach ($extensions as $ext) { $path = 'images/generated/'.$this->plugin->current_image.'.'.$ext; - if (\Illuminate\Support\Facades\Storage::disk('public')->exists($path)) { + if (Illuminate\Support\Facades\Storage::disk('public')->exists($path)) { return $path; } } diff --git a/resources/views/livewire/plugins/image-webhook.blade.php b/resources/views/livewire/plugins/image-webhook.blade.php index 788cbdb..c2db714 100644 --- a/resources/views/livewire/plugins/image-webhook.blade.php +++ b/resources/views/livewire/plugins/image-webhook.blade.php @@ -1,11 +1,13 @@ - ['name' => 'Markup', 'flux_icon_name' => 'code-bracket', 'detail_view_route' => 'plugins.markup'], - 'api' => - ['name' => 'API', 'flux_icon_name' => 'braces', 'detail_view_route' => 'plugins.api'], - 'image-webhook' => - ['name' => 'Image Webhook', 'flux_icon_name' => 'photo', 'detail_view_route' => 'plugins.image-webhook'], + 'markup' => ['name' => 'Markup', 'flux_icon_name' => 'code-bracket', 'detail_view_route' => 'plugins.markup'], + 'api' => ['name' => 'API', 'flux_icon_name' => 'braces', 'detail_view_route' => 'plugins.api'], + 'image-webhook' => ['name' => 'Image Webhook', 'flux_icon_name' => 'photo', 'detail_view_route' => 'plugins.image-webhook'], ]; protected $rules = [ @@ -60,29 +66,31 @@ new class extends Component { switch ($this->sortBy) { case 'name_asc': - usort($pluginsToSort, function($a, $b) { + usort($pluginsToSort, function ($a, $b) { return strcasecmp($a['name'] ?? '', $b['name'] ?? ''); }); break; case 'name_desc': - usort($pluginsToSort, function($a, $b) { + usort($pluginsToSort, function ($a, $b) { return strcasecmp($b['name'] ?? '', $a['name'] ?? ''); }); break; case 'date_desc': - usort($pluginsToSort, function($a, $b) { + usort($pluginsToSort, function ($a, $b) { $aDate = $a['created_at'] ?? '1970-01-01'; $bDate = $b['created_at'] ?? '1970-01-01'; + return strcmp($bDate, $aDate); }); break; case 'date_asc': - usort($pluginsToSort, function($a, $b) { + usort($pluginsToSort, function ($a, $b) { $aDate = $a['created_at'] ?? '1970-01-01'; $bDate = $b['created_at'] ?? '1970-01-01'; + return strcmp($aDate, $bDate); }); break; @@ -113,7 +121,7 @@ new class extends Component { abort_unless(auth()->user() !== null, 403); $this->validate(); - \App\Models\Plugin::create([ + App\Models\Plugin::create([ 'uuid' => Str::uuid(), 'user_id' => auth()->id(), 'name' => $this->name, @@ -137,7 +145,6 @@ new class extends Component { $this->refreshPlugins(); } - public function importZip(PluginImportService $pluginImportService): void { abort_unless(auth()->user() !== null, 403); @@ -153,11 +160,10 @@ new class extends Component { $this->reset(['zipFile']); Flux::modal('import-zip')->close(); - } catch (\Exception $e) { - $this->addError('zipFile', 'Error installing plugin: ' . $e->getMessage()); + } catch (Exception $e) { + $this->addError('zipFile', 'Error installing plugin: '.$e->getMessage()); } } - }; ?> diff --git a/resources/views/livewire/plugins/markup.blade.php b/resources/views/livewire/plugins/markup.blade.php index e78f137..150e626 100644 --- a/resources/views/livewire/plugins/markup.blade.php +++ b/resources/views/livewire/plugins/markup.blade.php @@ -4,12 +4,14 @@ use App\Jobs\GenerateScreenJob; use Illuminate\Support\Collection; use Livewire\Component; -new class extends Component { - +new class extends Component +{ public string $blade_code = ''; + public bool $isLoading = false; public Collection $devices; + public array $checked_devices; public function mount() @@ -17,17 +19,16 @@ new class extends Component { $this->devices = auth()->user()->devices->pluck('id', 'name'); } - public function submit() { $this->isLoading = true; $this->validate([ 'checked_devices' => 'required|array', - 'blade_code' => 'required|string' + 'blade_code' => 'required|string', ]); - //only devices that are owned by the user + // only devices that are owned by the user $this->checked_devices = array_intersect($this->checked_devices, auth()->user()->devices->pluck('id')->toArray()); try { @@ -35,7 +36,7 @@ new class extends Component { foreach ($this->checked_devices as $device) { GenerateScreenJob::dispatchSync($device, null, $rendered); } - } catch (\Exception $e) { + } catch (Exception $e) { $this->addError('generate_screen', $e->getMessage()); } @@ -66,7 +67,7 @@ new class extends Component { public function renderHelloWorld(): string { - return << @@ -84,7 +85,7 @@ HTML; public function renderQuote(): string { - return << @@ -102,7 +103,7 @@ HTML; public function renderTrainMonitor() { - return << @@ -136,7 +137,7 @@ HTML; public function renderHomeAssistant() { - return << @@ -162,8 +163,6 @@ HTML; HTML; } - - }; ?> diff --git a/resources/views/livewire/plugins/recipe.blade.php b/resources/views/livewire/plugins/recipe.blade.php index 6e13dfb..cda019e 100644 --- a/resources/views/livewire/plugins/recipe.blade.php +++ b/resources/views/livewire/plugins/recipe.blade.php @@ -1,44 +1,68 @@ plugin->render_markup_view) { try { - $basePath = resource_path('views/' . str_replace('.', '/', $this->plugin->render_markup_view)); + $basePath = resource_path('views/'.str_replace('.', '/', $this->plugin->render_markup_view)); $paths = [ - $basePath . '.blade.php', - $basePath . '.liquid', + $basePath.'.blade.php', + $basePath.'.liquid', ]; $this->view_content = null; @@ -63,7 +87,7 @@ new class extends Component { break; } } - } catch (\Exception $e) { + } catch (Exception $e) { $this->view_content = null; } } else { @@ -103,7 +127,7 @@ new class extends Component { $this->validate(); $this->plugin->update([ 'render_markup' => $this->markup_code ?? null, - 'markup_language' => $this->markup_language ?? null + 'markup_language' => $this->markup_language ?? null, ]); } @@ -136,7 +160,7 @@ new class extends Component { $this->validatePollingUrl(); $validated = $this->validate(); - $validated['data_payload'] = json_decode(Arr::get($validated,'data_payload'), true); + $validated['data_payload'] = json_decode(Arr::get($validated, 'data_payload'), true); $this->plugin->update($validated); foreach ($this->configuration_template as $fieldKey => $field) { @@ -144,7 +168,7 @@ new class extends Component { continue; } - if (!isset($this->multiValues[$fieldKey])) { + if (! isset($this->multiValues[$fieldKey])) { continue; } @@ -155,15 +179,15 @@ new class extends Component { protected function validatePollingUrl(): void { - if ($this->data_strategy === 'polling' && !empty($this->polling_url)) { + if ($this->data_strategy === 'polling' && ! empty($this->polling_url)) { try { $resolvedUrl = $this->plugin->resolveLiquidVariables($this->polling_url); - if (!filter_var($resolvedUrl, FILTER_VALIDATE_URL)) { + if (! filter_var($resolvedUrl, FILTER_VALIDATE_URL)) { $this->addError('polling_url', 'The polling URL must be a valid URL after resolving configuration variables.'); } - } catch (\Exception $e) { - $this->addError('polling_url', 'Error resolving Liquid variables: ' . $e->getMessage() . $e->getPrevious()?->getMessage()); + } catch (Exception $e) { + $this->addError('polling_url', 'Error resolving Liquid variables: '.$e->getMessage().$e->getPrevious()?->getMessage()); } } } @@ -177,8 +201,8 @@ new class extends Component { $this->data_payload = json_encode($this->plugin->data_payload, JSON_PRETTY_PRINT); $this->data_payload_updated_at = $this->plugin->data_payload_updated_at; - } catch (\Exception $e) { - $this->dispatch('data-update-error', message: $e->getMessage() . $e->getPrevious()?->getMessage()); + } catch (Exception $e) { + $this->dispatch('data-update-error', message: $e->getMessage().$e->getPrevious()?->getMessage()); } } } @@ -212,15 +236,17 @@ new class extends Component { // Validate that each checked device has a playlist selected foreach ($this->checked_devices as $deviceId) { - if (!isset($this->device_playlists[$deviceId]) || empty($this->device_playlists[$deviceId])) { - $this->addError('device_playlists.' . $deviceId, 'Please select a playlist for each device.'); + if (! isset($this->device_playlists[$deviceId]) || empty($this->device_playlists[$deviceId])) { + $this->addError('device_playlists.'.$deviceId, 'Please select a playlist for each device.'); + return; } // If creating new playlist, validate required fields if ($this->device_playlists[$deviceId] === 'new') { - if (!isset($this->device_playlist_names[$deviceId]) || empty($this->device_playlist_names[$deviceId])) { - $this->addError('device_playlist_names.' . $deviceId, 'Playlist name is required when creating a new playlist.'); + if (! isset($this->device_playlist_names[$deviceId]) || empty($this->device_playlist_names[$deviceId])) { + $this->addError('device_playlist_names.'.$deviceId, 'Playlist name is required when creating a new playlist.'); + return; } } @@ -231,15 +257,15 @@ new class extends Component { if ($this->device_playlists[$deviceId] === 'new') { // Create new playlist - $playlist = \App\Models\Playlist::create([ + $playlist = App\Models\Playlist::create([ 'device_id' => $deviceId, 'name' => $this->device_playlist_names[$deviceId], - 'weekdays' => !empty($this->device_weekdays[$deviceId] ?? null) ? $this->device_weekdays[$deviceId] : null, + 'weekdays' => ! empty($this->device_weekdays[$deviceId] ?? null) ? $this->device_weekdays[$deviceId] : null, 'active_from' => $this->device_active_from[$deviceId] ?? null, 'active_until' => $this->device_active_until[$deviceId] ?? null, ]); } else { - $playlist = \App\Models\Playlist::findOrFail($this->device_playlists[$deviceId]); + $playlist = App\Models\Playlist::findOrFail($this->device_playlists[$deviceId]); } // Add plugin to playlist @@ -253,11 +279,11 @@ new class extends Component { } else { // Create mashup $pluginIds = array_merge([$this->plugin->id], array_map('intval', $this->mashup_plugins)); - \App\Models\PlaylistItem::createMashup( + App\Models\PlaylistItem::createMashup( $playlist, $this->mashup_layout, $pluginIds, - $this->plugin->name . ' Mashup', + $this->plugin->name.' Mashup', $maxOrder + 1 ); } @@ -271,23 +297,24 @@ new class extends Component { 'device_active_from', 'device_active_until', 'mashup_layout', - 'mashup_plugins' + 'mashup_plugins', ]); Flux::modal('add-to-playlist')->close(); } public function getDevicePlaylists($deviceId) { - return \App\Models\Playlist::where('device_id', $deviceId)->get(); + return App\Models\Playlist::where('device_id', $deviceId)->get(); } public function hasAnyPlaylistSelected(): bool { foreach ($this->checked_devices as $deviceId) { - if (isset($this->device_playlists[$deviceId]) && !empty($this->device_playlists[$deviceId])) { + if (isset($this->device_playlists[$deviceId]) && ! empty($this->device_playlists[$deviceId])) { return true; } } + return false; } @@ -315,7 +342,7 @@ new class extends Component { public function renderLayoutWithTitleBar(): string { if ($this->markup_language === 'liquid') { - return <<
@@ -327,9 +354,9 @@ new class extends Component { HTML; } - return << 'full']) - + @@ -341,7 +368,7 @@ HTML; public function renderLayoutBlank(): string { if ($this->markup_language === 'liquid') { - return <<
@@ -350,9 +377,9 @@ HTML; HTML; } - return << 'full']) - + @@ -378,12 +405,12 @@ HTML; $this->dispatch('preview-updated', preview: $previewMarkup); } catch (LiquidException $e) { $this->dispatch('preview-error', message: $e->toLiquidErrorMessage()); - } catch (\Exception $e) { + } catch (Exception $e) { $this->dispatch('preview-error', message: $e->getMessage()); } } - private function createPreviewDevice(): \App\Models\Device + private function createPreviewDevice(): Device { $deviceModel = DeviceModel::with(['palette'])->find($this->preview_device_model_id) ?? DeviceModel::with(['palette'])->first(); @@ -434,18 +461,17 @@ HTML; #[Computed] private function parsedUrls() { - if (!isset($this->polling_url)) { + if (! isset($this->polling_url)) { return null; } try { return $this->plugin->resolveLiquidVariables($this->polling_url); - } catch (\Exception $e) { - return 'PARSE_ERROR: ' . $e->getMessage(); + } catch (Exception $e) { + return 'PARSE_ERROR: '.$e->getMessage(); } } - } ?> diff --git a/resources/views/livewire/plugins/recipes/settings.blade.php b/resources/views/livewire/plugins/recipes/settings.blade.php index e87ad78..c83f52c 100644 --- a/resources/views/livewire/plugins/recipes/settings.blade.php +++ b/resources/views/livewire/plugins/recipes/settings.blade.php @@ -7,10 +7,14 @@ use Livewire\Component; /* * This component contains the TRMNL Plugin Settings modal */ -new class extends Component { +new class extends Component +{ public Plugin $plugin; - public string|null $trmnlp_id = null; - public string|null $uuid = null; + + public ?string $trmnlp_id = null; + + public ?string $uuid = null; + public bool $alias = false; public int $resetIndex = 0; @@ -53,7 +57,7 @@ new class extends Component { { return url("/api/display/{$this->uuid}/alias"); } -};?> +}; ?>
diff --git a/resources/views/livewire/settings/appearance.blade.php b/resources/views/livewire/settings/appearance.blade.php index af056b0..145319d 100644 --- a/resources/views/livewire/settings/appearance.blade.php +++ b/resources/views/livewire/settings/appearance.blade.php @@ -2,7 +2,8 @@ use Livewire\Component; -new class extends Component { +new class extends Component +{ // }; ?> diff --git a/resources/views/livewire/settings/delete-user-form.blade.php b/resources/views/livewire/settings/delete-user-form.blade.php index 634f65b..8eed52b 100644 --- a/resources/views/livewire/settings/delete-user-form.blade.php +++ b/resources/views/livewire/settings/delete-user-form.blade.php @@ -4,7 +4,8 @@ use App\Livewire\Actions\Logout; use Illuminate\Support\Facades\Auth; use Livewire\Component; -new class extends Component { +new class extends Component +{ public string $password = ''; /** diff --git a/resources/views/livewire/settings/password.blade.php b/resources/views/livewire/settings/password.blade.php index 99b255f..fe0ef0b 100644 --- a/resources/views/livewire/settings/password.blade.php +++ b/resources/views/livewire/settings/password.blade.php @@ -6,9 +6,12 @@ use Illuminate\Validation\Rules\Password; use Illuminate\Validation\ValidationException; use Livewire\Component; -new class extends Component { +new class extends Component +{ public string $current_password = ''; + public string $password = ''; + public string $password_confirmation = ''; /** diff --git a/resources/views/livewire/settings/preferences.blade.php b/resources/views/livewire/settings/preferences.blade.php index 4d9f221..83b62a2 100644 --- a/resources/views/livewire/settings/preferences.blade.php +++ b/resources/views/livewire/settings/preferences.blade.php @@ -1,14 +1,11 @@ ignore($user->id) + Rule::unique(User::class)->ignore($user->id), ], ]); diff --git a/resources/views/livewire/settings/support.blade.php b/resources/views/livewire/settings/support.blade.php index 5c54e22..63d6a42 100644 --- a/resources/views/livewire/settings/support.blade.php +++ b/resources/views/livewire/settings/support.blade.php @@ -1,6 +1,7 @@
diff --git a/tests/Feature/Api/ImageWebhookTest.php b/tests/Feature/Api/ImageWebhookTest.php index 121f90a..545dae8 100644 --- a/tests/Feature/Api/ImageWebhookTest.php +++ b/tests/Feature/Api/ImageWebhookTest.php @@ -38,7 +38,7 @@ test('can upload image to image webhook plugin via multipart form', function (): // File should exist with the new UUID Storage::disk('public')->assertExists("images/generated/{$plugin->current_image}.png"); - + // Image URL should contain the new UUID expect($response->json('image_url')) ->toContain($plugin->current_image); @@ -70,7 +70,7 @@ test('can upload image to image webhook plugin via raw binary', function (): voi // File should exist with the new UUID Storage::disk('public')->assertExists("images/generated/{$plugin->current_image}.png"); - + // Image URL should contain the new UUID expect($response->json('image_url')) ->toContain($plugin->current_image); @@ -102,7 +102,7 @@ test('can upload image to image webhook plugin via base64 data URI', function () // File should exist with the new UUID Storage::disk('public')->assertExists("images/generated/{$plugin->current_image}.png"); - + // Image URL should contain the new UUID expect($response->json('image_url')) ->toContain($plugin->current_image); diff --git a/tests/Feature/Livewire/Plugins/ConfigModalTest.php b/tests/Feature/Livewire/Plugins/ConfigModalTest.php index 0807d8e..517d130 100644 --- a/tests/Feature/Livewire/Plugins/ConfigModalTest.php +++ b/tests/Feature/Livewire/Plugins/ConfigModalTest.php @@ -22,9 +22,9 @@ test('config modal correctly loads multi_string defaults into UI boxes', functio 'field_type' => 'multi_string', 'name' => 'Reading Days', 'default' => 'alpha,beta', - ]] + ]], ], - 'configuration' => ['tags' => 'alpha,beta'] + 'configuration' => ['tags' => 'alpha,beta'], ]); Livewire::test('plugins.config-modal', ['plugin' => $plugin]) @@ -45,8 +45,8 @@ test('config modal validates against commas in multi_string boxes', function (): 'keyname' => 'tags', 'field_type' => 'multi_string', 'name' => 'Reading Days', - ]] - ] + ]], + ], ]); Livewire::test('plugins.config-modal', ['plugin' => $plugin]) @@ -72,9 +72,9 @@ test('config modal merges multi_string boxes into a single CSV string on save', 'keyname' => 'items', 'field_type' => 'multi_string', 'name' => 'Reading Days', - ]] + ]], ], - 'configuration' => [] + 'configuration' => [], ]); Livewire::test('plugins.config-modal', ['plugin' => $plugin]) @@ -96,7 +96,7 @@ test('config modal resetForm clears dirty state and increments resetIndex', func 'user_id' => $user->id, 'name' => 'Test Plugin', 'data_strategy' => 'static', - 'configuration' => ['simple_key' => 'original_value'] + 'configuration' => ['simple_key' => 'original_value'], ]); Livewire::test('plugins.config-modal', ['plugin' => $plugin]) @@ -114,7 +114,7 @@ test('config modal dispatches update event for parent warning refresh', function 'uuid' => Str::uuid(), 'user_id' => $user->id, 'name' => 'Test Plugin', - 'data_strategy' => 'static' + 'data_strategy' => 'static', ]); Livewire::test('plugins.config-modal', ['plugin' => $plugin]) From b097b0a7d7ce57faf1dedc3c5efbb0b2e3530a78 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Thu, 15 Jan 2026 13:14:50 +0100 Subject: [PATCH 3/7] chore: phpstan fixes, rector --- app/Liquid/Filters/StringMarkup.php | 2 +- app/Models/Plugin.php | 6 +++--- app/Services/ImageGenerationService.php | 8 ++++---- app/Services/PluginImportService.php | 8 ++++---- .../{Volt => Livewire/Catalog}/CatalogTrmnlTest.php | 0 tests/Feature/{Volt => Livewire}/DevicePalettesTest.php | 4 +--- tests/Feature/PluginImportTest.php | 4 ++-- 7 files changed, 15 insertions(+), 17 deletions(-) rename tests/Feature/{Volt => Livewire/Catalog}/CatalogTrmnlTest.php (100%) rename tests/Feature/{Volt => Livewire}/DevicePalettesTest.php (99%) diff --git a/app/Liquid/Filters/StringMarkup.php b/app/Liquid/Filters/StringMarkup.php index 10c5abc..6cdf4c0 100644 --- a/app/Liquid/Filters/StringMarkup.php +++ b/app/Liquid/Filters/StringMarkup.php @@ -73,7 +73,7 @@ class StringMarkup extends FiltersProvider // Default module_size is 11 // Size calculation: (21 modules for QR code + 4 modules margin on each side * 2) * module_size // = (21 + 8) * module_size = 29 * module_size - $moduleSize = $moduleSize ?? 11; + $moduleSize ??= 11; $size = 29 * $moduleSize; $qrCode = QrCode::format('svg') diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index 68f8e7e..31841bc 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -174,8 +174,8 @@ class Plugin extends Model // resolve and clean URLs $resolvedPollingUrls = $this->resolveLiquidVariables($this->polling_url); $urls = array_values(array_filter( // array_values ensures 0, 1, 2... - array_map('trim', explode("\n", $resolvedPollingUrls)), - fn ($url): bool => filled($url) + array_map(trim(...), explode("\n", $resolvedPollingUrls)), + filled(...) )); $combinedResponse = []; @@ -624,7 +624,7 @@ class Plugin extends Model // File doesn't exist, remove the view reference $attributes['render_markup_view'] = null; } - } catch (Exception $e) { + } catch (Exception) { // If file reading fails, remove the view reference $attributes['render_markup_view'] = null; } diff --git a/app/Services/ImageGenerationService.php b/app/Services/ImageGenerationService.php index 405ea3f..87fb6d9 100644 --- a/app/Services/ImageGenerationService.php +++ b/app/Services/ImageGenerationService.php @@ -61,9 +61,9 @@ class ImageGenerationService try { // Get image generation settings from DeviceModel or Device (for legacy devices) - $imageSettings = $deviceModel + $imageSettings = $deviceModel instanceof DeviceModel ? self::getImageSettingsFromModel($deviceModel) - : ($device ? self::getImageSettings($device) : self::getImageSettingsFromModel(null)); + : ($device instanceof Device ? self::getImageSettings($device) : self::getImageSettingsFromModel(null)); $fileExtension = $imageSettings['mime_type'] === 'image/bmp' ? 'bmp' : 'png'; $outputPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.'.$fileExtension); @@ -78,7 +78,7 @@ class ImageGenerationService $browserStage->html($markup); // Set timezone from user or fall back to app timezone - $timezone = $user?->timezone ?? config('app.timezone'); + $timezone = $user->timezone ?? config('app.timezone'); $browserStage->timezone($timezone); if (config('app.puppeteer_window_size_strategy') === 'v2') { @@ -186,7 +186,7 @@ class ImageGenerationService */ private static function getImageSettingsFromModel(?DeviceModel $deviceModel): array { - if ($deviceModel) { + if ($deviceModel instanceof DeviceModel) { return [ 'width' => $deviceModel->width, 'height' => $deviceModel->height, diff --git a/app/Services/PluginImportService.php b/app/Services/PluginImportService.php index 49dce99..51a9aee 100644 --- a/app/Services/PluginImportService.php +++ b/app/Services/PluginImportService.php @@ -33,11 +33,11 @@ class PluginImportService foreach ($settings['custom_fields'] as $field) { if (isset($field['field_type']) && $field['field_type'] === 'multi_string') { - if (isset($field['default']) && str_contains($field['default'], ',')) { + if (isset($field['default']) && str_contains((string) $field['default'], ',')) { throw new Exception("Validation Error: The default value for multistring fields like `{$field['keyname']}` cannot contain commas."); } - if (isset($field['placeholder']) && str_contains($field['placeholder'], ',')) { + if (isset($field['placeholder']) && str_contains((string) $field['placeholder'], ',')) { throw new Exception("Validation Error: The placeholder value for multistring fields like `{$field['keyname']}` cannot contain commas."); } @@ -159,7 +159,7 @@ class PluginImportService : null, 'polling_body' => $settings['polling_body'] ?? null, 'markup_language' => $markupLanguage, - 'render_markup' => $fullLiquid, + 'render_markup' => $fullLiquid ?? null, 'configuration_template' => $configurationTemplate, 'data_payload' => json_decode($settings['static_data'] ?? '{}', true), ]); @@ -321,7 +321,7 @@ class PluginImportService : null, 'polling_body' => $settings['polling_body'] ?? null, 'markup_language' => $markupLanguage, - 'render_markup' => $fullLiquid, + 'render_markup' => $fullLiquid ?? null, 'configuration_template' => $configurationTemplate, 'data_payload' => json_decode($settings['static_data'] ?? '{}', true), 'preferred_renderer' => $preferredRenderer, diff --git a/tests/Feature/Volt/CatalogTrmnlTest.php b/tests/Feature/Livewire/Catalog/CatalogTrmnlTest.php similarity index 100% rename from tests/Feature/Volt/CatalogTrmnlTest.php rename to tests/Feature/Livewire/Catalog/CatalogTrmnlTest.php diff --git a/tests/Feature/Volt/DevicePalettesTest.php b/tests/Feature/Livewire/DevicePalettesTest.php similarity index 99% rename from tests/Feature/Volt/DevicePalettesTest.php rename to tests/Feature/Livewire/DevicePalettesTest.php index f94708e..3c37934 100644 --- a/tests/Feature/Volt/DevicePalettesTest.php +++ b/tests/Feature/Livewire/DevicePalettesTest.php @@ -156,9 +156,7 @@ test('can delete a device palette', function (): void { ->call('deleteDevicePalette', $palette->id); expect(DevicePalette::find($palette->id))->toBeNull(); - $component->assertSet('devicePalettes', function ($palettes) use ($palette) { - return $palettes->where('id', $palette->id)->isEmpty(); - }); + $component->assertSet('devicePalettes', fn ($palettes) => $palettes->where('id', $palette->id)->isEmpty()); }); test('can duplicate a device palette', function (): void { diff --git a/tests/Feature/PluginImportTest.php b/tests/Feature/PluginImportTest.php index f3ef1fa..cc47e67 100644 --- a/tests/Feature/PluginImportTest.php +++ b/tests/Feature/PluginImportTest.php @@ -467,7 +467,7 @@ YAML; $zipFile = UploadedFile::fake()->createWithContent('invalid-default.zip', $zipContent); $pluginImportService = new PluginImportService(); - expect(fn () => $pluginImportService->importFromZip($zipFile, $user)) + expect(fn (): Plugin => $pluginImportService->importFromZip($zipFile, $user)) ->toThrow(Exception::class, 'Validation Error: The default value for multistring fields like `api_key` cannot contain commas.'); }); @@ -497,7 +497,7 @@ YAML; $zipFile = UploadedFile::fake()->createWithContent('invalid-placeholder.zip', $zipContent); $pluginImportService = new PluginImportService(); - expect(fn () => $pluginImportService->importFromZip($zipFile, $user)) + expect(fn (): Plugin => $pluginImportService->importFromZip($zipFile, $user)) ->toThrow(Exception::class, 'Validation Error: The placeholder value for multistring fields like `api_key` cannot contain commas.'); }); From e660da46fb2a793ac429b6d7f189a1e66a7b9ba7 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Thu, 15 Jan 2026 21:55:24 +0100 Subject: [PATCH 4/7] 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 }} -
diff --git a/resources/views/livewire/settings/profile.blade.php b/resources/views/pages/settings/profile.blade.php similarity index 56% rename from resources/views/livewire/settings/profile.blade.php rename to resources/views/pages/settings/profile.blade.php index c10682e..e8da089 100644 --- a/resources/views/livewire/settings/profile.blade.php +++ b/resources/views/pages/settings/profile.blade.php @@ -1,13 +1,17 @@ validate([ - 'name' => ['required', 'string', 'max:255'], - - 'email' => [ - 'required', - 'string', - 'lowercase', - 'email', - 'max:255', - Rule::unique(User::class)->ignore($user->id), - ], - ]); + $validated = $this->validate($this->profileRules($user->id)); $user->fill($validated); @@ -69,35 +62,47 @@ new class extends Component Session::flash('status', 'verification-link-sent'); } + + #[Computed] + public function hasUnverifiedEmail(): bool + { + return Auth::user() instanceof MustVerifyEmail && ! Auth::user()->hasVerifiedEmail(); + } + + #[Computed] + public function showDeleteUser(): bool + { + return ! Auth::user() instanceof MustVerifyEmail + || (Auth::user() instanceof MustVerifyEmail && Auth::user()->hasVerifiedEmail()); + } }; ?>
@include('partials.settings-heading') - + {{ __('Profile Settings') }} + +
- +
- + - @if (auth()->user() instanceof \Illuminate\Contracts\Auth\MustVerifyEmail &&! auth()->user()->hasVerifiedEmail()) + @if ($this->hasUnverifiedEmail)
-

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

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

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

+ @endif
@endif @@ -105,7 +110,9 @@ new class extends Component
- {{ __('Save') }} + + {{ __('Save') }} +
@@ -114,6 +121,8 @@ new class extends Component
- - + @if ($this->showDeleteUser) + + @endif +
diff --git a/resources/views/livewire/settings/support.blade.php b/resources/views/pages/settings/support.blade.php similarity index 91% rename from resources/views/livewire/settings/support.blade.php rename to resources/views/pages/settings/support.blade.php index 63d6a42..bf3d088 100644 --- a/resources/views/livewire/settings/support.blade.php +++ b/resources/views/pages/settings/support.blade.php @@ -7,7 +7,7 @@ new class extends Component {}
@include('partials.settings-heading') - +
@@ -35,5 +35,5 @@ new class extends Component {}
-
+
diff --git a/resources/views/pages/settings/two-factor.blade.php b/resources/views/pages/settings/two-factor.blade.php new file mode 100644 index 0000000..7d5f1f7 --- /dev/null +++ b/resources/views/pages/settings/two-factor.blade.php @@ -0,0 +1,388 @@ +user()->two_factor_confirmed_at)) { + $disableTwoFactorAuthentication(auth()->user()); + } + + $this->twoFactorEnabled = auth()->user()->hasEnabledTwoFactorAuthentication(); + $this->requiresConfirmation = Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm'); + } + + /** + * Enable two-factor authentication for the user. + */ + public function enable(EnableTwoFactorAuthentication $enableTwoFactorAuthentication): void + { + $enableTwoFactorAuthentication(auth()->user()); + + if (! $this->requiresConfirmation) { + $this->twoFactorEnabled = auth()->user()->hasEnabledTwoFactorAuthentication(); + } + + $this->loadSetupData(); + + $this->showModal = true; + } + + /** + * Load the two-factor authentication setup data for the user. + */ + private function loadSetupData(): void + { + $user = auth()->user(); + + try { + $this->qrCodeSvg = $user?->twoFactorQrCodeSvg(); + $this->manualSetupKey = decrypt($user->two_factor_secret); + } catch (Exception) { + $this->addError('setupData', 'Failed to fetch setup data.'); + + $this->reset('qrCodeSvg', 'manualSetupKey'); + } + } + + /** + * Show the two-factor verification step if necessary. + */ + public function showVerificationIfNecessary(): void + { + if ($this->requiresConfirmation) { + $this->showVerificationStep = true; + + $this->resetErrorBag(); + + return; + } + + $this->closeModal(); + } + + /** + * Confirm two-factor authentication for the user. + */ + public function confirmTwoFactor(ConfirmTwoFactorAuthentication $confirmTwoFactorAuthentication): void + { + $this->validate(); + + $confirmTwoFactorAuthentication(auth()->user(), $this->code); + + $this->closeModal(); + + $this->twoFactorEnabled = true; + } + + /** + * Reset two-factor verification state. + */ + public function resetVerification(): void + { + $this->reset('code', 'showVerificationStep'); + + $this->resetErrorBag(); + } + + /** + * Disable two-factor authentication for the user. + */ + public function disable(DisableTwoFactorAuthentication $disableTwoFactorAuthentication): void + { + $disableTwoFactorAuthentication(auth()->user()); + + $this->twoFactorEnabled = false; + } + + /** + * Close the two-factor authentication modal. + */ + public function closeModal(): void + { + $this->reset( + 'code', + 'manualSetupKey', + 'qrCodeSvg', + 'showModal', + 'showVerificationStep', + ); + + $this->resetErrorBag(); + + if (! $this->requiresConfirmation) { + $this->twoFactorEnabled = auth()->user()->hasEnabledTwoFactorAuthentication(); + } + } + + /** + * Get the current modal configuration state. + */ + public function getModalConfigProperty(): array + { + if ($this->twoFactorEnabled) { + return [ + 'title' => __('Two-Factor Authentication Enabled'), + 'description' => __('Two-factor authentication is now enabled. Scan the QR code or enter the setup key in your authenticator app.'), + 'buttonText' => __('Close'), + ]; + } + + if ($this->showVerificationStep) { + return [ + 'title' => __('Verify Authentication Code'), + 'description' => __('Enter the 6-digit code from your authenticator app.'), + 'buttonText' => __('Continue'), + ]; + } + + return [ + 'title' => __('Enable Two-Factor Authentication'), + 'description' => __('To finish enabling two-factor authentication, scan the QR code or enter the setup key in your authenticator app.'), + 'buttonText' => __('Continue'), + ]; + } +} ?> + +
+ @include('partials.settings-heading') + + {{ __('Two-Factor Authentication Settings') }} + + +
+ @if ($twoFactorEnabled) +
+
+ {{ __('Enabled') }} +
+ + + {{ __('With two-factor authentication enabled, you will be prompted for a secure, random pin during login, which you can retrieve from the TOTP-supported application on your phone.') }} + + + + +
+ + {{ __('Disable 2FA') }} + +
+
+ @else +
+
+ {{ __('Disabled') }} +
+ + + {{ __('When you enable two-factor authentication, you will be prompted for a secure pin during login. This pin can be retrieved from a TOTP-supported application on your phone.') }} + + + + {{ __('Enable 2FA') }} + +
+ @endif +
+
+ + +
+
+
+
+
+ @for ($i = 1; $i <= 5; $i++) +
+ @endfor +
+ +
+ @for ($i = 1; $i <= 5; $i++) +
+ @endfor +
+ + +
+
+ +
+ {{ $this->modalConfig['title'] }} + {{ $this->modalConfig['description'] }} +
+
+ + @if ($showVerificationStep) +
+
+ +
+ +
+ + {{ __('Back') }} + + + + {{ __('Confirm') }} + +
+
+ @else + @error('setupData') + + @enderror + +
+
+ @empty($qrCodeSvg) +
+ +
+ @else +
+
+ {!! $qrCodeSvg !!} +
+
+ @endempty +
+
+ +
+ + {{ $this->modalConfig['buttonText'] }} + +
+ +
+
+
+ + {{ __('or, enter the code manually') }} + +
+ +
+
+ @empty($manualSetupKey) +
+ +
+ @else + + + + @endempty +
+
+
+ @endif +
+
+
diff --git a/resources/views/pages/settings/two-factor/recovery-codes.blade.php b/resources/views/pages/settings/two-factor/recovery-codes.blade.php new file mode 100644 index 0000000..763259f --- /dev/null +++ b/resources/views/pages/settings/two-factor/recovery-codes.blade.php @@ -0,0 +1,136 @@ +loadRecoveryCodes(); + } + + /** + * Generate new recovery codes for the user. + */ + public function regenerateRecoveryCodes(GenerateNewRecoveryCodes $generateNewRecoveryCodes): void + { + $generateNewRecoveryCodes(auth()->user()); + + $this->loadRecoveryCodes(); + } + + /** + * Load the recovery codes for the user. + */ + private function loadRecoveryCodes(): void + { + $user = auth()->user(); + + if ($user->hasEnabledTwoFactorAuthentication() && $user->two_factor_recovery_codes) { + try { + $this->recoveryCodes = json_decode(decrypt($user->two_factor_recovery_codes), true); + } catch (Exception) { + $this->addError('recoveryCodes', 'Failed to load recovery codes'); + + $this->recoveryCodes = []; + } + } + } +}; ?> + +
+
+
+ + {{ __('2FA Recovery Codes') }} +
+ + {{ __('Recovery codes let you regain access if you lose your 2FA device. Store them in a secure password manager.') }} + +
+ +
+
+ + + + {{ __('Hide Recovery Codes') }} + + + @if (filled($recoveryCodes)) + + {{ __('Regenerate Codes') }} + + @endif +
+ +
+
+ @error('recoveryCodes') + + @enderror + + @if (filled($recoveryCodes)) +
+ @foreach($recoveryCodes as $code) +
+ {{ $code }} +
+ @endforeach +
+ + {{ __('Each recovery code can be used once to access your account and will be removed after use. If you need more, click Regenerate Codes above.') }} + + @endif +
+
+
+
diff --git a/resources/views/partials/settings-heading.blade.php b/resources/views/partials/settings-heading.blade.php index cd175c3..925ace9 100644 --- a/resources/views/partials/settings-heading.blade.php +++ b/resources/views/partials/settings-heading.blade.php @@ -1,5 +1,5 @@
- Settings - Manage your profile and account settings + {{ __('Settings') }} + {{ __('Manage your profile and account settings') }}
diff --git a/routes/auth.php b/routes/auth.php index 4f493e2..c5108e0 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -1,24 +1,10 @@ group(function () { - Route::livewire('login', 'auth.login') - ->name('login'); - - if (config('app.registration.enabled')) { - Route::livewire('register', 'auth.register') - ->name('register'); - } - - Route::livewire('forgot-password', 'auth.forgot-password') - ->name('password.request'); - - Route::livewire('reset-password/{token}', 'auth.reset-password') - ->name('password.reset'); - // OIDC authentication routes Route::get('auth/oidc/redirect', [OidcController::class, 'redirect']) ->name('auth.oidc.redirect'); @@ -27,18 +13,3 @@ Route::middleware('guest')->group(function () { ->name('auth.oidc.callback'); }); - -Route::middleware('auth')->group(function () { - Route::livewire('verify-email', 'auth.verify-email') - ->name('verification.notice'); - - Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) - ->middleware(['signed', 'throttle:6,1']) - ->name('verification.verify'); - - Route::livewire('confirm-password', 'auth.confirm-password') - ->name('password.confirm'); -}); - -Route::post('logout', App\Livewire\Actions\Logout::class) - ->name('logout'); diff --git a/routes/settings.php b/routes/settings.php new file mode 100644 index 0000000..e575755 --- /dev/null +++ b/routes/settings.php @@ -0,0 +1,23 @@ +group(function () { + Route::redirect('settings', 'settings/profile'); + Route::livewire('settings/preferences', 'pages::settings.preferences')->name('settings.preferences'); + Route::livewire('settings/profile', 'pages::settings.profile')->name('profile.edit'); + Route::livewire('settings/password', 'pages::settings.password')->name('user-password.edit'); + Route::livewire('settings/appearance', 'pages::settings.appearance')->name('appearance.edit'); + Route::livewire('settings/two-factor', 'pages::settings.two-factor') + ->middleware( + when( + Features::canManageTwoFactorAuthentication() + && Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword'), + ['password.confirm'], + [], + ), + ) + ->name('two-factor.show'); + Route::livewire('settings/support', 'pages::settings.support')->name('settings.support'); +}); diff --git a/routes/web.php b/routes/web.php index d7007e4..08b8cef 100644 --- a/routes/web.php +++ b/routes/web.php @@ -9,12 +9,6 @@ Route::get('/', function () { })->name('home'); Route::middleware(['auth'])->group(function () { - Route::redirect('settings', 'settings/preferences'); - Route::livewire('settings/preferences', 'settings.preferences')->name('settings.preferences'); - Route::livewire('settings/profile', 'settings.profile')->name('settings.profile'); - Route::livewire('settings/password', 'settings.password')->name('settings.password'); - Route::livewire('settings/appearance', 'settings.appearance')->name('settings.appearance'); - Route::livewire('settings/support', 'settings.support')->name('settings.support'); Route::livewire('/dashboard', 'device-dashboard')->name('dashboard'); @@ -44,3 +38,4 @@ Route::middleware(['auth'])->group(function () { }); require __DIR__.'/auth.php'; +require __DIR__.'/settings.php'; diff --git a/tests/Feature/Auth/AuthenticationTest.php b/tests/Feature/Auth/AuthenticationTest.php index 68c9648..91a0fd1 100644 --- a/tests/Feature/Auth/AuthenticationTest.php +++ b/tests/Feature/Auth/AuthenticationTest.php @@ -13,13 +13,13 @@ test('login screen can be rendered', function (): void { test('users can authenticate using the login screen', function (): void { $user = User::factory()->create(); - $response = Livewire::test('auth.login') - ->set('email', $user->email) - ->set('password', 'password') - ->call('login'); + $response = $this->post(route('login.store'), [ + 'email' => $user->email, + 'password' => 'password', + ]); $response - ->assertHasNoErrors() + ->assertSessionHasNoErrors() ->assertRedirect(route('dashboard', absolute: false)); $this->assertAuthenticated(); diff --git a/tests/Feature/Auth/EmailVerificationTest.php b/tests/Feature/Auth/EmailVerificationTest.php index 5cc2db8..cc5b126 100644 --- a/tests/Feature/Auth/EmailVerificationTest.php +++ b/tests/Feature/Auth/EmailVerificationTest.php @@ -10,9 +10,9 @@ uses(Illuminate\Foundation\Testing\RefreshDatabase::class); test('email verification screen can be rendered', function (): void { $user = User::factory()->unverified()->create(); - $response = $this->actingAs($user)->get('/verify-email'); + $response = $this->actingAs($user)->get(route('verification.notice')); - $response->assertStatus(200); + $response->assertOk(); }); test('email can be verified', function (): void { diff --git a/tests/Feature/Auth/PasswordConfirmationTest.php b/tests/Feature/Auth/PasswordConfirmationTest.php index 6896206..4b5197f 100644 --- a/tests/Feature/Auth/PasswordConfirmationTest.php +++ b/tests/Feature/Auth/PasswordConfirmationTest.php @@ -7,33 +7,7 @@ uses(Illuminate\Foundation\Testing\RefreshDatabase::class); test('confirm password screen can be rendered', function (): void { $user = User::factory()->create(); - $response = $this->actingAs($user)->get('/confirm-password'); + $response = $this->actingAs($user)->get(route('password.confirm')); - $response->assertStatus(200); -}); - -test('password can be confirmed', function (): void { - $user = User::factory()->create(); - - $this->actingAs($user); - - $response = Livewire::test('auth.confirm-password') - ->set('password', 'password') - ->call('confirmPassword'); - - $response - ->assertHasNoErrors() - ->assertRedirect(route('dashboard', absolute: false)); -}); - -test('password is not confirmed with invalid password', function (): void { - $user = User::factory()->create(); - - $this->actingAs($user); - - $response = Livewire::test('auth.confirm-password') - ->set('password', 'wrong-password') - ->call('confirmPassword'); - - $response->assertHasErrors(['password']); + $response->assertOk(); }); diff --git a/tests/Feature/Auth/PasswordResetTest.php b/tests/Feature/Auth/PasswordResetTest.php index b53f103..df60c4b 100644 --- a/tests/Feature/Auth/PasswordResetTest.php +++ b/tests/Feature/Auth/PasswordResetTest.php @@ -17,9 +17,7 @@ test('reset password link can be requested', function (): void { $user = User::factory()->create(); - Livewire::test('auth.forgot-password') - ->set('email', $user->email) - ->call('sendPasswordResetLink'); + $this->post(route('password.request'), ['email' => $user->email]); Notification::assertSentTo($user, ResetPassword::class); }); @@ -29,14 +27,12 @@ test('reset password screen can be rendered', function (): void { $user = User::factory()->create(); - Livewire::test('auth.forgot-password') - ->set('email', $user->email) - ->call('sendPasswordResetLink'); + $this->post(route('password.request'), ['email' => $user->email]); Notification::assertSentTo($user, ResetPassword::class, function ($notification): true { - $response = $this->get('/reset-password/'.$notification->token); + $response = $this->get(route('password.reset', $notification->token)); - $response->assertStatus(200); + $response->assertOk(); return true; }); @@ -47,19 +43,18 @@ test('password can be reset with valid token', function (): void { $user = User::factory()->create(); - Livewire::test('auth.forgot-password') - ->set('email', $user->email) - ->call('sendPasswordResetLink'); + $this->post(route('password.request'), ['email' => $user->email]); Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user): true { - $response = Livewire::test('auth.reset-password', ['token' => $notification->token]) - ->set('email', $user->email) - ->set('password', 'password') - ->set('password_confirmation', 'password') - ->call('resetPassword'); + $response = $this->post(route('password.update'), [ + 'token' => $notification->token, + 'email' => $user->email, + 'password' => 'password', + 'password_confirmation' => 'password', + ]); $response - ->assertHasNoErrors() + ->assertSessionHasNoErrors() ->assertRedirect(route('login', absolute: false)); return true; diff --git a/tests/Feature/Auth/RegistrationTest.php b/tests/Feature/Auth/RegistrationTest.php index 2f931be..3a216d5 100644 --- a/tests/Feature/Auth/RegistrationTest.php +++ b/tests/Feature/Auth/RegistrationTest.php @@ -3,21 +3,20 @@ uses(Illuminate\Foundation\Testing\RefreshDatabase::class); test('registration screen can be rendered', function (): void { - $response = $this->get('/register'); + $response = $this->get(route('register')); - $response->assertStatus(200); + $response->assertOk(); }); test('new users can register', function (): void { - $response = Livewire::test('auth.register') - ->set('name', 'Test User') - ->set('email', 'test@example.com') - ->set('password', 'password') - ->set('password_confirmation', 'password') - ->call('register'); + $response = $this->post(route('register.store'), [ + 'name' => 'John Doe', + 'email' => 'test@example.com', + 'password' => 'password', + 'password_confirmation' => 'password', + ]); - $response - ->assertHasNoErrors() + $response->assertSessionHasNoErrors() ->assertRedirect(route('dashboard', absolute: false)); $this->assertAuthenticated(); diff --git a/tests/Feature/Auth/TwoFactorChallengeTest.php b/tests/Feature/Auth/TwoFactorChallengeTest.php new file mode 100644 index 0000000..43b2f9a --- /dev/null +++ b/tests/Feature/Auth/TwoFactorChallengeTest.php @@ -0,0 +1,34 @@ +markTestSkipped('Two-factor authentication is not enabled.'); + } + + $response = $this->get(route('two-factor.login')); + + $response->assertRedirect(route('login')); +}); + +test('two_factor_challenge_can_be_rendered', function (): void { + if (! Features::canManageTwoFactorAuthentication()) { + $this->markTestSkipped('Two-factor authentication is not enabled.'); + } + + Features::twoFactorAuthentication([ + 'confirm' => true, + 'confirmPassword' => true, + ]); + + $user = User::factory()->withTwoFactor()->create(); + + $this->post(route('login.store'), [ + 'email' => $user->email, + 'password' => 'password', + ])->assertRedirect(route('two-factor.login')); +}); diff --git a/tests/Feature/Settings/PasswordUpdateTest.php b/tests/Feature/Settings/PasswordUpdateTest.php index 0c40594..aab8311 100644 --- a/tests/Feature/Settings/PasswordUpdateTest.php +++ b/tests/Feature/Settings/PasswordUpdateTest.php @@ -12,7 +12,7 @@ test('password can be updated', function (): void { $this->actingAs($user); - $response = Livewire::test('settings.password') + $response = Livewire::test('pages::settings.password') ->set('current_password', 'password') ->set('password', 'new-password') ->set('password_confirmation', 'new-password') @@ -20,7 +20,7 @@ test('password can be updated', function (): void { $response->assertHasNoErrors(); - expect(Hash::check('new-password', $user->refresh()->password))->toBeTrue(); + $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); }); test('correct password must be provided to update password', function (): void { @@ -30,7 +30,7 @@ test('correct password must be provided to update password', function (): void { $this->actingAs($user); - $response = Livewire::test('settings.password') + $response = Livewire::test('pages::settings.password') ->set('current_password', 'wrong-password') ->set('password', 'new-password') ->set('password_confirmation', 'new-password') diff --git a/tests/Feature/Settings/ProfileUpdateTest.php b/tests/Feature/Settings/ProfileUpdateTest.php index 8bb21af..e37e63e 100644 --- a/tests/Feature/Settings/ProfileUpdateTest.php +++ b/tests/Feature/Settings/ProfileUpdateTest.php @@ -15,7 +15,7 @@ test('profile information can be updated', function (): void { $this->actingAs($user); - $response = Livewire::test('settings.profile') + $response = Livewire::test('pages::settings.profile') ->set('name', 'Test User') ->set('email', 'test@example.com') ->call('updateProfileInformation'); @@ -34,7 +34,7 @@ test('email verification status is unchanged when email address is unchanged', f $this->actingAs($user); - $response = Livewire::test('settings.profile') + $response = Livewire::test('pages::settings.profile') ->set('name', 'Test User') ->set('email', $user->email) ->call('updateProfileInformation'); @@ -49,7 +49,7 @@ test('user can delete their account', function (): void { $this->actingAs($user); - $response = Livewire::test('settings.delete-user-form') + $response = Livewire::test('pages::settings.delete-user-form') ->set('password', 'password') ->call('deleteUser'); @@ -66,7 +66,7 @@ test('correct password must be provided to delete account', function (): void { $this->actingAs($user); - $response = Livewire::test('settings.delete-user-form') + $response = Livewire::test('pages::settings.delete-user-form') ->set('password', 'wrong-password') ->call('deleteUser'); From 33b1083770a65b76a3dcd6d8d3f32c234323b457 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Thu, 15 Jan 2026 22:19:03 +0100 Subject: [PATCH 5/7] refactor: qr-code filter --- app/Facades/QrCode.php | 24 +++++ app/Liquid/Filters/StringMarkup.php | 23 +---- app/Providers/AppServiceProvider.php | 2 +- app/Services/QrCodeService.php | 147 +++++++++++++++++++++++++++ 4 files changed, 174 insertions(+), 22 deletions(-) create mode 100644 app/Facades/QrCode.php create mode 100644 app/Services/QrCodeService.php diff --git a/app/Facades/QrCode.php b/app/Facades/QrCode.php new file mode 100644 index 0000000..00de934 --- /dev/null +++ b/app/Facades/QrCode.php @@ -0,0 +1,24 @@ +errorCorrection($errorCorrection); } - $svg = (string) $qrCode->generate($text); - - // Add class="qr-code" to the SVG element - // The SVG may start with and then - if (preg_match('/]*)>/', $svg, $matches)) { - $attributes = $matches[1]; - // Check if class already exists - if (mb_strpos($attributes, 'class=') === false) { - $svg = preg_replace('/]*)>/', '', $svg, 1); - } else { - // If class exists, add qr-code to it - $svg = preg_replace('/(]*class=["\'])([^"\']*)(["\'][^>]*>)/', '$1$2 qr-code$3', $svg, 1); - } - } else { - // Fallback: simple replacement if no attributes - $svg = preg_replace('//', '', $svg, 1); - } - - return $svg; + return $qrCode->generate($text); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 78adbde..48178e8 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -16,7 +16,7 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - $this->app->bind('qr-code', fn (): QrCodeService => new QrCodeService); + $this->app->bind('qr-code', fn () => new QrCodeService); } /** diff --git a/app/Services/QrCodeService.php b/app/Services/QrCodeService.php new file mode 100644 index 0000000..812415b --- /dev/null +++ b/app/Services/QrCodeService.php @@ -0,0 +1,147 @@ +format = $format; + + return $this; + } + + /** + * Set the size of the QR code + * + * @param int $size The size in pixels + * @return $this + */ + public function size(int $size): self + { + $this->size = $size; + + return $this; + } + + /** + * Set the error correction level + * + * @param string $level Error correction level: 'l', 'm', 'q', 'h' + * @return $this + */ + public function errorCorrection(string $level): self + { + $this->errorCorrection = $level; + + return $this; + } + + /** + * Generate the QR code + * + * @param string $text The text to encode + * @return string The generated QR code (SVG string) + */ + public function generate(string $text): string + { + // Ensure format is set (default to SVG) + $format = $this->format ?? 'svg'; + + if ($format !== 'svg') { + throw new InvalidArgumentException("Format '{$format}' is not supported. Only 'svg' is currently supported."); + } + + // Calculate size and margin + // If size is not set, calculate from module size (default module size is 11) + if ($this->size === null) { + $moduleSize = 11; + $this->size = 29 * $moduleSize; + } + + // Calculate margin: 4 modules on each side + // Module size = size / 29, so margin = (size / 29) * 4 + $moduleSize = $this->size / 29; + $margin = (int) ($moduleSize * 4); + + // Map error correction level + $errorCorrectionLevel = ErrorCorrectionLevel::valueOf('M'); // default + if ($this->errorCorrection !== null) { + $errorCorrectionLevel = match (mb_strtoupper($this->errorCorrection)) { + 'L' => ErrorCorrectionLevel::valueOf('L'), + 'M' => ErrorCorrectionLevel::valueOf('M'), + 'Q' => ErrorCorrectionLevel::valueOf('Q'), + 'H' => ErrorCorrectionLevel::valueOf('H'), + default => ErrorCorrectionLevel::valueOf('M'), + }; + } + + // Create renderer style with size and margin + $rendererStyle = new RendererStyle($this->size, $margin); + + // Create SVG renderer + $renderer = new ImageRenderer( + $rendererStyle, + new SvgImageBackEnd() + ); + + // Create writer + $writer = new Writer($renderer); + + // Generate SVG + $svg = $writer->writeString($text, 'ISO-8859-1', $errorCorrectionLevel); + + // Add class="qr-code" to the SVG element + $svg = $this->addQrCodeClass($svg); + + return $svg; + } + + /** + * Add the 'qr-code' class to the SVG element + * + * @param string $svg The SVG string + * @return string The SVG string with the class added + */ + protected function addQrCodeClass(string $svg): string + { + // Match + if (preg_match('/]*)>/', $svg, $matches)) { + $attributes = $matches[1]; + // Check if class already exists + if (mb_strpos($attributes, 'class=') === false) { + $svg = preg_replace('/]*)>/', '', $svg, 1); + } else { + // If class exists, add qr-code to it + $svg = preg_replace('/(]*class=["\'])([^"\']*)(["\'][^>]*>)/', '$1$2 qr-code$3', $svg, 1); + } + } else { + // Fallback: simple replacement if no attributes + $svg = preg_replace('//', '', $svg, 1); + } + + return $svg; + } +} From 11d2c1c4baddb69d6820a21412303e980847685b Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Thu, 15 Jan 2026 22:50:28 +0100 Subject: [PATCH 6/7] chore: publish livewire config --- config/livewire.php | 277 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 config/livewire.php diff --git a/config/livewire.php b/config/livewire.php new file mode 100644 index 0000000..4c68f45 --- /dev/null +++ b/config/livewire.php @@ -0,0 +1,277 @@ + [ + resource_path('views/components'), + resource_path('views/livewire'), + ], + + /* + |--------------------------------------------------------------------------- + | Component Namespaces + |--------------------------------------------------------------------------- + | + | This value sets default namespaces that will be used to resolve view-based + | components like single-file and multi-file components. These folders'll + | also be referenced when creating new components via the make command. + | + */ + + 'component_namespaces' => [ + 'layouts' => resource_path('views/layouts'), + 'pages' => resource_path('views/pages'), + ], + + /* + |--------------------------------------------------------------------------- + | Page Layout + |--------------------------------------------------------------------------- + | The view that will be used as the layout when rendering a single component as + | an entire page via `Route::livewire('/post/create', 'pages::create-post')`. + | In this case, the content of pages::create-post will render into $slot. + | + */ + + 'component_layout' => 'layouts::app', + + /* + |--------------------------------------------------------------------------- + | Lazy Loading Placeholder + |--------------------------------------------------------------------------- + | Livewire allows you to lazy load components that would otherwise slow down + | the initial page load. Every component can have a custom placeholder or + | you can define the default placeholder view for all components below. + | + */ + + 'component_placeholder' => null, // Example: 'placeholders::skeleton' + + /* + |--------------------------------------------------------------------------- + | Make Command + |--------------------------------------------------------------------------- + | This value determines the default configuration for the artisan make command + | You can configure the component type (sfc, mfc, class) and whether to use + | the high-voltage (⚡) emoji as a prefix in the sfc|mfc component names. + | + */ + + 'make_command' => [ + 'type' => 'sfc', // Options: 'sfc', 'mfc', 'class' + 'emoji' => false, // Options: true, false + ], + + /* + |--------------------------------------------------------------------------- + | Class Namespace + |--------------------------------------------------------------------------- + | + | This value sets the root class namespace for Livewire component classes in + | your application. This value will change where component auto-discovery + | finds components. It's also referenced by the file creation commands. + | + */ + + 'class_namespace' => 'App\\Livewire', + + /* + |--------------------------------------------------------------------------- + | Class Path + |--------------------------------------------------------------------------- + | + | This value is used to specify the path where Livewire component class files + | are created when running creation commands like `artisan make:livewire`. + | This path is customizable to match your projects directory structure. + | + */ + + 'class_path' => app_path('Livewire'), + + /* + |--------------------------------------------------------------------------- + | View Path + |--------------------------------------------------------------------------- + | + | This value is used to specify where Livewire component Blade templates are + | stored when running file creation commands like `artisan make:livewire`. + | It is also used if you choose to omit a component's render() method. + | + */ + + 'view_path' => resource_path('views/livewire'), + + /* + |--------------------------------------------------------------------------- + | Temporary File Uploads + |--------------------------------------------------------------------------- + | + | Livewire handles file uploads by storing uploads in a temporary directory + | before the file is stored permanently. All file uploads are directed to + | a global endpoint for temporary storage. You may configure this below: + | + */ + + 'temporary_file_upload' => [ + 'disk' => env('LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK'), // Example: 'local', 's3' | Default: 'default' + 'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB) + 'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp' + 'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1' + 'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs... + 'png', 'gif', 'bmp', 'svg', 'wav', 'mp4', + 'mov', 'avi', 'wmv', 'mp3', 'm4a', + 'jpg', 'jpeg', 'mpga', 'webp', 'wma', + ], + 'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated... + 'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs... + ], + + /* + |--------------------------------------------------------------------------- + | Render On Redirect + |--------------------------------------------------------------------------- + | + | This value determines if Livewire will run a component's `render()` method + | after a redirect has been triggered using something like `redirect(...)` + | Setting this to true will render the view once more before redirecting + | + */ + + 'render_on_redirect' => false, + + /* + |--------------------------------------------------------------------------- + | Eloquent Model Binding + |--------------------------------------------------------------------------- + | + | Previous versions of Livewire supported binding directly to eloquent model + | properties using wire:model by default. However, this behavior has been + | deemed too "magical" and has therefore been put under a feature flag. + | + */ + + 'legacy_model_binding' => false, + + /* + |--------------------------------------------------------------------------- + | Auto-inject Frontend Assets + |--------------------------------------------------------------------------- + | + | By default, Livewire automatically injects its JavaScript and CSS into the + | and of pages containing Livewire components. By disabling + | this behavior, you need to use @livewireStyles and @livewireScripts. + | + */ + + 'inject_assets' => true, + + /* + |--------------------------------------------------------------------------- + | Navigate (SPA mode) + |--------------------------------------------------------------------------- + | + | By adding `wire:navigate` to links in your Livewire application, Livewire + | will prevent the default link handling and instead request those pages + | via AJAX, creating an SPA-like effect. Configure this behavior here. + | + */ + + 'navigate' => [ + 'show_progress_bar' => true, + 'progress_bar_color' => '#E05B45', + ], + + /* + |--------------------------------------------------------------------------- + | HTML Morph Markers + |--------------------------------------------------------------------------- + | + | Livewire intelligently "morphs" existing HTML into the newly rendered HTML + | after each update. To make this process more reliable, Livewire injects + | "markers" into the rendered Blade surrounding @if, @class & @foreach. + | + */ + + 'inject_morph_markers' => true, + + /* + |--------------------------------------------------------------------------- + | Smart Wire Keys + |--------------------------------------------------------------------------- + | + | Livewire uses loops and keys used within loops to generate smart keys that + | are applied to nested components that don't have them. This makes using + | nested components more reliable by ensuring that they all have keys. + | + */ + + 'smart_wire_keys' => true, + + /* + |--------------------------------------------------------------------------- + | Pagination Theme + |--------------------------------------------------------------------------- + | + | When enabling Livewire's pagination feature by using the `WithPagination` + | trait, Livewire will use Tailwind templates to render pagination views + | on the page. If you want Bootstrap CSS, you can specify: "bootstrap" + | + */ + + 'pagination_theme' => 'tailwind', + + /* + |--------------------------------------------------------------------------- + | Release Token + |--------------------------------------------------------------------------- + | + | This token is stored client-side and sent along with each request to check + | a users session to see if a new release has invalidated it. If there is + | a mismatch it will throw an error and prompt for a browser refresh. + | + */ + + 'release_token' => 'a', + + /* + |--------------------------------------------------------------------------- + | CSP Safe + |--------------------------------------------------------------------------- + | + | This config is used to determine if Livewire will use the CSP-safe version + | of Alpine in its bundle. This is useful for applications that are using + | strict Content Security Policy (CSP) to protect against XSS attacks. + | + */ + + 'csp_safe' => false, + + /* + |--------------------------------------------------------------------------- + | Payload Guards + |--------------------------------------------------------------------------- + | + | These settings protect against malicious or oversized payloads that could + | cause denial of service. The default values should feel reasonable for + | most web applications. Each can be set to null to disable the limit. + | + */ + + 'payload' => [ + 'max_size' => 1024 * 1024, // 1MB - maximum request payload size in bytes + 'max_nesting_depth' => 10, // Maximum depth of dot-notation property paths + 'max_calls' => 50, // Maximum method calls per request + 'max_components' => 20, // Maximum components per batch request + ], +]; From 82079b21d5dc1c1c7c3dde40d1d8fa939e702c94 Mon Sep 17 00:00:00 2001 From: jerremyng Date: Thu, 15 Jan 2026 16:38:09 +0000 Subject: [PATCH 7/7] add dependencies on dev container --- .devcontainer/cli/Dockerfile | 9 +++++++-- .devcontainer/fpm/Dockerfile | 10 ++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.devcontainer/cli/Dockerfile b/.devcontainer/cli/Dockerfile index ab13330..d2c44b1 100644 --- a/.devcontainer/cli/Dockerfile +++ b/.devcontainer/cli/Dockerfile @@ -10,7 +10,10 @@ RUN apk add --no-cache composer RUN apk add --no-cache \ imagemagick-dev \ chromium \ - libzip-dev + libzip-dev \ + freetype-dev \ + libpng-dev \ + libjpeg-turbo-dev ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium ENV PUPPETEER_DOCKER=1 @@ -19,8 +22,10 @@ RUN mkdir -p /usr/src/php/ext/imagick RUN chmod 777 /usr/src/php/ext/imagick RUN curl -fsSL https://github.com/Imagick/imagick/archive/refs/tags/3.8.0.tar.gz | tar xvz -C "/usr/src/php/ext/imagick" --strip 1 +RUN docker-php-ext-configure gd --with-freetype --with-jpeg + # Install PHP extensions -RUN docker-php-ext-install imagick zip +RUN docker-php-ext-install imagick zip gd # Composer uses its php binary, but we want it to use the container's one RUN rm -f /usr/bin/php84 diff --git a/.devcontainer/fpm/Dockerfile b/.devcontainer/fpm/Dockerfile index 3e658b6..d8ce6cc 100644 --- a/.devcontainer/fpm/Dockerfile +++ b/.devcontainer/fpm/Dockerfile @@ -15,7 +15,11 @@ RUN apk add --no-cache \ npm \ imagemagick-dev \ chromium \ - libzip-dev + libzip-dev \ + freetype-dev \ + libpng-dev \ + libjpeg-turbo-dev + ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium ENV PUPPETEER_DOCKER=1 @@ -24,8 +28,10 @@ RUN mkdir -p /usr/src/php/ext/imagick RUN chmod 777 /usr/src/php/ext/imagick RUN curl -fsSL https://github.com/Imagick/imagick/archive/refs/tags/3.8.0.tar.gz | tar xvz -C "/usr/src/php/ext/imagick" --strip 1 +RUN docker-php-ext-configure gd --with-freetype --with-jpeg + # Install PHP extensions -RUN docker-php-ext-install imagick zip +RUN docker-php-ext-install imagick zip gd RUN rm -f /usr/bin/php84 RUN ln -s /usr/local/bin/php /usr/bin/php84