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 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/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/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/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/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..48178e8 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 () => 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/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 @@ -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/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; + } +} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 90915e8..0ad9c57 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -2,6 +2,5 @@ return [ App\Providers\AppServiceProvider::class, - App\Providers\FolioServiceProvider::class, - App\Providers\VoltServiceProvider::class, + App\Providers\FortifyServiceProvider::class, ]; diff --git a/composer.json b/composer.json index f59e0f3..a4951a0 100644 --- a/composer.json +++ b/composer.json @@ -18,15 +18,14 @@ "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", "laravel/tinker": "^2.10.1", - "livewire/livewire": "^3.7", "livewire/flux": "^2.0", - "livewire/volt": "^1.7", + "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 e5840ef..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": "ac6b1e352cb66f858a50b64e7e3c70d0", + "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", @@ -3082,16 +3146,16 @@ }, { "name": "livewire/livewire", - "version": "v3.7.4", + "version": "v4.0.1", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "5a8dffd4c0ab357ff7ed5b39e7c2453d962a68e0" + "reference": "c7539589d5af82691bef17da17ce4e289269f8d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/5a8dffd4c0ab357ff7ed5b39e7c2453d962a68e0", - "reference": "5a8dffd4c0ab357ff7ed5b39e7c2453d962a68e0", + "url": "https://api.github.com/repos/livewire/livewire/zipball/c7539589d5af82691bef17da17ce4e289269f8d9", + "reference": "c7539589d5af82691bef17da17ce4e289269f8d9", "shasum": "" }, "require": { @@ -3146,7 +3210,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.7.4" + "source": "https://github.com/livewire/livewire/tree/v4.0.1" }, "funding": [ { @@ -3154,78 +3218,7 @@ "type": "github" } ], - "time": "2026-01-13T09:37:21+00:00" - }, - { - "name": "livewire/volt", - "version": "v1.10.1", - "source": { - "type": "git", - "url": "https://github.com/livewire/volt.git", - "reference": "48cff133990c6261c63ee279fc091af6f6c6654e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/livewire/volt/zipball/48cff133990c6261c63ee279fc091af6f6c6654e", - "reference": "48cff133990c6261c63ee279fc091af6f6c6654e", - "shasum": "" - }, - "require": { - "laravel/framework": "^10.38.2|^11.0|^12.0", - "livewire/livewire": "^3.6.1|^4.0", - "php": "^8.1" - }, - "require-dev": { - "laravel/folio": "^1.1", - "orchestra/testbench": "^8.36|^9.15|^10.8", - "pestphp/pest": "^2.9.5|^3.0|^4.0", - "phpstan/phpstan": "^1.10" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "Livewire\\Volt\\VoltServiceProvider" - ] - }, - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "files": [ - "functions.php" - ], - "psr-4": { - "Livewire\\Volt\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - }, - { - "name": "Nuno Maduro", - "email": "nuno@laravel.com" - } - ], - "description": "An elegantly crafted functional API for Laravel Livewire.", - "homepage": "https://github.com/livewire/volt", - "keywords": [ - "laravel", - "livewire", - "volt" - ], - "support": { - "issues": "https://github.com/livewire/volt/issues", - "source": "https://github.com/livewire/volt" - }, - "time": "2025-11-25T16:19:15+00:00" + "time": "2026-01-14T18:40:41+00:00" }, { "name": "maennchen/zipstream-php", @@ -4233,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", @@ -4922,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/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 + ], +]; 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/layouts/auth.blade.php b/resources/views/components/layouts/auth.blade.php deleted file mode 100644 index 4ddd14d..0000000 --- a/resources/views/components/layouts/auth.blade.php +++ /dev/null @@ -1,3 +0,0 @@ - - {{ $slot }} - 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 cecbabe..1c94dfb 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') }} @@ -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,14 +23,14 @@ {{ $heading }} -