From 254a7d46a08889facbf419a9fea4a9da628d7564 Mon Sep 17 00:00:00 2001 From: nexxo Date: Sat, 25 Jul 2026 01:20:25 +0200 Subject: [PATCH] feat(portal): Fortify auth + Login/2FA/Dashboard + component kit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend (Fortify): - laravel/fortify with TOTP two-factor + recovery codes; User uses TwoFactorAuthenticatable; 2FA credentials hidden from serialization. - Views off — pages are full-page class-based Livewire components (R1/R2); Fortify handles POST actions. Home redirect -> /dashboard. v1 scope: login + 2FA only (no public register/reset/passkeys). Seeder gated to local/testing. Component kit (Blade, token-based, a11y): - button, input, checkbox, alert, card, badge, stat-tile, otp-input (Alpine, auto-advance/paste, -safe submit), progress-stepper, nav-item, icon (Lucide), plus layouts/portal-app app-shell (sidebar drawer + topbar + menu). Screens (localized DE/EN, R16): - Login (form -> login.store), Two-factor challenge (OTP + recovery fallback), Dashboard (KPI stat tiles, instance card, provisioning stepper fixtures, activity). Routes English (R13). Tests + verification: - Pest: 14 green (login ok/invalid/throttle, dashboard guard, component render). - R12 browser (Puppeteer, prod assets): /, /login, /two-factor-challenge and the authenticated /dashboard all HTTP 200 with ZERO console errors; login flow verified end-to-end. - Test isolation fixed (force test env over injected .env). - Reviewed with Codex (R15): 4 rounds, all findings fixed, final pass clean. Co-Authored-By: Claude Opus 4.8 --- app/Actions/Fortify/CreateNewUser.php | 43 + .../Fortify/PasswordValidationRules.php | 19 + app/Actions/Fortify/ResetUserPassword.php | 32 + app/Actions/Fortify/UpdateUserPassword.php | 35 + .../Fortify/UpdateUserProfileInformation.php | 61 + app/Livewire/Auth/Login.php | 15 + app/Livewire/Auth/TwoFactorChallenge.php | 15 + app/Livewire/Dashboard.php | 44 + app/Models/User.php | 5 +- app/Providers/FortifyServiceProvider.php | 56 + bootstrap/providers.php | 5 +- composer.json | 3 + composer.lock | 2209 ++++++++++++++++- config/fortify.php | 183 ++ ..._add_two_factor_columns_to_users_table.php | 42 + ...026_07_24_224531_create_passkeys_table.php | 35 + database/seeders/DatabaseSeeder.php | 20 +- lang/de/auth.php | 26 + lang/de/dashboard.php | 60 + lang/en/auth.php | 26 + lang/en/dashboard.php | 60 + phpunit.xml | 22 +- resources/css/app.css | 5 + resources/js/app.js | 56 +- resources/views/components/ui/alert.blade.php | 12 + resources/views/components/ui/badge.blade.php | 28 + .../views/components/ui/button.blade.php | 39 + resources/views/components/ui/card.blade.php | 14 + .../views/components/ui/checkbox.blade.php | 15 + resources/views/components/ui/icon.blade.php | 25 + resources/views/components/ui/input.blade.php | 36 + .../views/components/ui/nav-item.blade.php | 23 + .../views/components/ui/otp-input.blade.php | 30 + .../components/ui/progress-stepper.blade.php | 31 + .../views/components/ui/stat-tile.blade.php | 29 + resources/views/layouts/portal-app.blade.php | 90 + resources/views/livewire/auth/login.blade.php | 41 + .../auth/two-factor-challenge.blade.php | 39 + resources/views/livewire/dashboard.blade.php | 61 + routes/web.php | 16 +- tests/Feature/Auth/LoginTest.php | 52 + tests/Feature/Auth/TwoFactorChallengeTest.php | 8 + tests/Feature/ComponentTest.php | 30 + tests/Feature/DashboardTest.php | 16 + tests/Feature/WelcomeTest.php | 8 + tests/Pest.php | 50 + 46 files changed, 3729 insertions(+), 41 deletions(-) create mode 100644 app/Actions/Fortify/CreateNewUser.php create mode 100644 app/Actions/Fortify/PasswordValidationRules.php create mode 100644 app/Actions/Fortify/ResetUserPassword.php create mode 100644 app/Actions/Fortify/UpdateUserPassword.php create mode 100644 app/Actions/Fortify/UpdateUserProfileInformation.php create mode 100644 app/Livewire/Auth/Login.php create mode 100644 app/Livewire/Auth/TwoFactorChallenge.php create mode 100644 app/Livewire/Dashboard.php create mode 100644 app/Providers/FortifyServiceProvider.php create mode 100644 config/fortify.php create mode 100644 database/migrations/2026_07_24_224530_add_two_factor_columns_to_users_table.php create mode 100644 database/migrations/2026_07_24_224531_create_passkeys_table.php create mode 100644 lang/de/auth.php create mode 100644 lang/de/dashboard.php create mode 100644 lang/en/auth.php create mode 100644 lang/en/dashboard.php create mode 100644 resources/views/components/ui/alert.blade.php create mode 100644 resources/views/components/ui/badge.blade.php create mode 100644 resources/views/components/ui/button.blade.php create mode 100644 resources/views/components/ui/card.blade.php create mode 100644 resources/views/components/ui/checkbox.blade.php create mode 100644 resources/views/components/ui/icon.blade.php create mode 100644 resources/views/components/ui/input.blade.php create mode 100644 resources/views/components/ui/nav-item.blade.php create mode 100644 resources/views/components/ui/otp-input.blade.php create mode 100644 resources/views/components/ui/progress-stepper.blade.php create mode 100644 resources/views/components/ui/stat-tile.blade.php create mode 100644 resources/views/layouts/portal-app.blade.php create mode 100644 resources/views/livewire/auth/login.blade.php create mode 100644 resources/views/livewire/auth/two-factor-challenge.blade.php create mode 100644 resources/views/livewire/dashboard.blade.php create mode 100644 tests/Feature/Auth/LoginTest.php create mode 100644 tests/Feature/Auth/TwoFactorChallengeTest.php create mode 100644 tests/Feature/ComponentTest.php create mode 100644 tests/Feature/DashboardTest.php create mode 100644 tests/Feature/WelcomeTest.php create mode 100644 tests/Pest.php diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php new file mode 100644 index 0000000..ee2d712 --- /dev/null +++ b/app/Actions/Fortify/CreateNewUser.php @@ -0,0 +1,43 @@ + $input + * + * @throws ValidationException + */ + public function create(array $input): User + { + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + Rule::unique(User::class), + ], + 'password' => $this->passwordRules(), + ])->validate(); + + return User::create([ + 'name' => $input['name'], + 'email' => $input['email'], + 'password' => Hash::make($input['password']), + ]); + } +} diff --git a/app/Actions/Fortify/PasswordValidationRules.php b/app/Actions/Fortify/PasswordValidationRules.php new file mode 100644 index 0000000..3678865 --- /dev/null +++ b/app/Actions/Fortify/PasswordValidationRules.php @@ -0,0 +1,19 @@ +|string> + */ + protected function passwordRules(): array + { + return ['required', 'string', Password::default(), 'confirmed']; + } +} diff --git a/app/Actions/Fortify/ResetUserPassword.php b/app/Actions/Fortify/ResetUserPassword.php new file mode 100644 index 0000000..667651f --- /dev/null +++ b/app/Actions/Fortify/ResetUserPassword.php @@ -0,0 +1,32 @@ + $input + * + * @throws ValidationException + */ + public function reset(User $user, array $input): void + { + Validator::make($input, [ + 'password' => $this->passwordRules(), + ])->validate(); + + $user->forceFill([ + 'password' => Hash::make($input['password']), + ])->save(); + } +} diff --git a/app/Actions/Fortify/UpdateUserPassword.php b/app/Actions/Fortify/UpdateUserPassword.php new file mode 100644 index 0000000..4a0306d --- /dev/null +++ b/app/Actions/Fortify/UpdateUserPassword.php @@ -0,0 +1,35 @@ + $input + * + * @throws ValidationException + */ + public function update(User $user, array $input): void + { + Validator::make($input, [ + 'current_password' => ['required', 'string', 'current_password:web'], + 'password' => $this->passwordRules(), + ], [ + 'current_password.current_password' => __('The provided password does not match your current password.'), + ])->validateWithBag('updatePassword'); + + $user->forceFill([ + 'password' => Hash::make($input['password']), + ])->save(); + } +} diff --git a/app/Actions/Fortify/UpdateUserProfileInformation.php b/app/Actions/Fortify/UpdateUserProfileInformation.php new file mode 100644 index 0000000..62f58fa --- /dev/null +++ b/app/Actions/Fortify/UpdateUserProfileInformation.php @@ -0,0 +1,61 @@ + $input + * + * @throws ValidationException + */ + public function update(User $user, array $input): void + { + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + Rule::unique('users')->ignore($user->id), + ], + ])->validateWithBag('updateProfileInformation'); + + if ($input['email'] !== $user->email && + $user instanceof MustVerifyEmail) { + $this->updateVerifiedUser($user, $input); + } else { + $user->forceFill([ + 'name' => $input['name'], + 'email' => $input['email'], + ])->save(); + } + } + + /** + * Update the given verified user's profile information. + * + * @param array $input + */ + protected function updateVerifiedUser(User $user, array $input): void + { + $user->forceFill([ + 'name' => $input['name'], + 'email' => $input['email'], + 'email_verified_at' => null, + ])->save(); + + $user->sendEmailVerificationNotification(); + } +} diff --git a/app/Livewire/Auth/Login.php b/app/Livewire/Auth/Login.php new file mode 100644 index 0000000..cf5bd6a --- /dev/null +++ b/app/Livewire/Auth/Login.php @@ -0,0 +1,15 @@ += 90 ? 'danger' : ($storagePercent >= 85 ? 'warning' : 'accent'); + + return view('livewire.dashboard', [ + 'storageUsed' => $storageUsed, + 'storageQuota' => $storageQuota, + 'storagePercent' => $storagePercent, + 'storageTone' => $storageTone, + 'activeUsers' => 14, + 'lastBackup' => __('dashboard.today_time', ['time' => '04:12']), + 'uptime' => '99.98', + 'instanceUrl' => 'https://kunde-demo.clupilot.com', + 'instanceStatus' => 'active', + 'provisioning' => false, + 'steps' => [ + ['label' => __('dashboard.steps.validate'), 'state' => 'done'], + ['label' => __('dashboard.steps.provision'), 'state' => 'done'], + ['label' => __('dashboard.steps.configure'), 'state' => 'done'], + ['label' => __('dashboard.steps.acceptance'), 'state' => 'done'], + ], + 'activity' => [ + ['label' => __('dashboard.activity_items.backup_ok'), 'time' => '04:12'], + ['label' => __('dashboard.activity_items.user_added'), 'time' => __('dashboard.yesterday')], + ['label' => __('dashboard.activity_items.login'), 'time' => __('dashboard.yesterday')], + ], + ]); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index f6ba1d2..04b3b7c 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -9,13 +9,14 @@ use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Laravel\Fortify\TwoFactorAuthenticatable; #[Fillable(['name', 'email', 'password'])] -#[Hidden(['password', 'remember_token'])] +#[Hidden(['password', 'remember_token', 'two_factor_secret', 'two_factor_recovery_codes'])] class User extends Authenticatable { /** @use HasFactory */ - use HasFactory, Notifiable; + use HasFactory, Notifiable, TwoFactorAuthenticatable; /** * Get the attributes that should be cast. diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php new file mode 100644 index 0000000..e48ab1a --- /dev/null +++ b/app/Providers/FortifyServiceProvider.php @@ -0,0 +1,56 @@ +input(Fortify::username())).'|'.$request->ip()); + + return Limit::perMinute(5)->by($throttleKey); + }); + + RateLimiter::for('two-factor', function (Request $request) { + return Limit::perMinute(5)->by($request->session()->get('login.id')); + }); + + RateLimiter::for('passkeys', function (Request $request) { + $credentialId = $request->input('credential.id'); + + return Limit::perMinute(10)->by( + ($credentialId ?: $request->session()->getId()).'|'.$request->ip() + ); + }); + } +} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index fc94ae6..0ad9c57 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -1,7 +1,6 @@ =7.1 <9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.7" + }, + "time": "2025-09-16T12:23:56+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -339,6 +444,54 @@ }, "time": "2024-07-08T12:26:09+00:00" }, + { + "name": "doctrine/deprecations", + "version": "1.1.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, { "name": "doctrine/inflector", "version": "2.1.0", @@ -1234,6 +1387,70 @@ ], "time": "2026-07-17T13:53:03+00:00" }, + { + "name": "laravel/fortify", + "version": "v1.37.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/fortify.git", + "reference": "66b9503330e7c18b3edebb9c3b4087037826573b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/fortify/zipball/66b9503330e7c18b3edebb9c3b4087037826573b", + "reference": "66b9503330e7c18b3edebb9c3b4087037826573b", + "shasum": "" + }, + "require": { + "bacon/bacon-qr-code": "^3.0", + "ext-json": "*", + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "laravel/passkeys": "^0.2.0", + "php": "^8.2", + "pragmarx/google2fa": "^9.0" + }, + "require-dev": { + "orchestra/testbench": "^9.15|^10.8|^11.0", + "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": "2026-06-29T16:22:02+00:00" + }, { "name": "laravel/framework", "version": "v13.22.0", @@ -1461,6 +1678,74 @@ }, "time": "2026-07-24T20:38:48+00:00" }, + { + "name": "laravel/passkeys", + "version": "v0.2.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/passkeys-server.git", + "reference": "a76656ada41b2b4a591f075eddae5ddc67e8ab9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/passkeys-server/zipball/a76656ada41b2b4a591f075eddae5ddc67e8ab9c", + "reference": "a76656ada41b2b4a591f075eddae5ddc67e8ab9c", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/database": "^11.0|^12.0|^13.0", + "illuminate/http": "^11.0|^12.0|^13.0", + "illuminate/routing": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "web-auth/webauthn-lib": "5.3.x" + }, + "require-dev": { + "laravel/pint": "^1.28.0", + "orchestra/testbench": "^9.0|^10.0|^11.0", + "pestphp/pest": "^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "rector/rector": "^2.3" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Passkeys\\PasskeysServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Passkeys\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Passwordless authentication using WebAuthn/passkeys for Laravel", + "homepage": "https://github.com/laravel/passkeys-server", + "keywords": [ + "Authentication", + "Passwordless", + "laravel", + "passkeys", + "webauthn" + ], + "support": { + "issues": "https://github.com/laravel/passkeys-server/issues", + "source": "https://github.com/laravel/passkeys-server" + }, + "time": "2026-05-18T16:26:00+00:00" + }, { "name": "laravel/prompts", "version": "v0.3.21", @@ -2993,6 +3278,182 @@ }, "time": "2020-10-15T08:29:30+00:00" }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^2.0", + "phpstan/phpdoc-parser": "^2.0", + "webmozart/assert": "^1.9.1 || ^2" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26", + "shipmonk/dead-code-detector": "^0.5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" + }, + "time": "2026-03-18T20:49:53+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0" + }, + "time": "2026-01-06T21:53:42+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.5", @@ -3178,6 +3639,105 @@ ], "time": "2026-06-14T23:24:10+00:00" }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", + "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.3" + }, + "time": "2026-07-08T07:01:06+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", @@ -4577,6 +5137,186 @@ ], "time": "2026-05-19T14:06:37+00:00" }, + { + "name": "spomky-labs/cbor-php", + "version": "3.3.0", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/cbor-php.git", + "reference": "013d13da69cf28b1ae501887daceccc850ca1c76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/013d13da69cf28b1ae501887daceccc850ca1c76", + "reference": "013d13da69cf28b1ae501887daceccc850ca1c76", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17|^0.18", + "ext-mbstring": "*", + "php": ">=8.0" + }, + "require-dev": { + "ext-json": "*", + "roave/security-advisories": "dev-latest", + "symfony/error-handler": "^6.4|^7.1|^8.0", + "symfony/var-dumper": "^6.4|^7.1|^8.0" + }, + "suggest": { + "ext-bcmath": "GMP or BCMath extensions will drastically improve the library performance. BCMath extension needed to handle the Big Float and Decimal Fraction Tags", + "ext-gmp": "GMP or BCMath extensions will drastically improve the library performance" + }, + "type": "library", + "autoload": { + "psr-4": { + "CBOR\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/Spomky-Labs/cbor-php/contributors" + } + ], + "description": "CBOR Encoder/Decoder for PHP", + "keywords": [ + "Concise Binary Object Representation", + "RFC7049", + "cbor" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/cbor-php/issues", + "source": "https://github.com/Spomky-Labs/cbor-php/tree/3.3.0" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2026-07-15T18:56:27+00:00" + }, + { + "name": "spomky-labs/pki-framework", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/pki-framework.git", + "reference": "e0d61661962560c1cedfef02b51b431e720aae78" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/e0d61661962560c1cedfef02b51b431e720aae78", + "reference": "e0d61661962560c1cedfef02b51b431e720aae78", + "shasum": "" + }, + "require": { + "brick/math": "^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17|^0.18", + "ext-mbstring": "*", + "php": ">=8.1" + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0|^2.0|^3.0", + "ext-gmp": "*", + "ext-openssl": "*", + "infection/infection": "^0.28|^0.29|^0.31", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/extension-installer": "^1.3|^2.0", + "phpstan/phpstan": "^1.8|^2.0", + "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", + "phpstan/phpstan-phpunit": "^1.1|^2.0", + "phpstan/phpstan-strict-rules": "^1.3|^2.0", + "phpunit/phpunit": "^10.1|^11.0|^12.0", + "rector/rector": "^1.0|^2.0", + "roave/security-advisories": "dev-latest", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symplify/easy-coding-standard": "^12.0 || ^13.0" + }, + "suggest": { + "ext-bcmath": "For better performance (or GMP)", + "ext-gmp": "For better performance (or BCMath)", + "ext-openssl": "For OpenSSL based cyphering" + }, + "type": "library", + "autoload": { + "psr-4": { + "SpomkyLabs\\Pki\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joni Eskelinen", + "email": "jonieske@gmail.com", + "role": "Original developer" + }, + { + "name": "Florent Morselli", + "email": "florent.morselli@spomky-labs.com", + "role": "Spomky-Labs PKI Framework developer" + } + ], + "description": "A PHP framework for managing Public Key Infrastructures. It comprises X.509 public key certificates, attribute certificates, certification requests and certification path validation.", + "homepage": "https://github.com/spomky-labs/pki-framework", + "keywords": [ + "DER", + "Private Key", + "ac", + "algorithm identifier", + "asn.1", + "asn1", + "attribute certificate", + "certificate", + "certification request", + "cryptography", + "csr", + "decrypt", + "ec", + "encrypt", + "pem", + "pkcs", + "public key", + "rsa", + "sign", + "signature", + "verify", + "x.509", + "x.690", + "x509", + "x690" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/pki-framework/issues", + "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.5.0" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2026-07-16T10:28:45+00:00" + }, { "name": "symfony/clock", "version": "v7.4.8", @@ -6556,6 +7296,177 @@ ], "time": "2026-05-23T16:05:06+00:00" }, + { + "name": "symfony/property-access", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc", + "reference": "b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/property-info": "^6.4.32|~7.3.10|^7.4.4|^8.0.4" + }, + "require-dev": { + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4.1|^7.0.1|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/property-info", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "ac5e82528b986c4f7cfccbf7764b5d2e824d6175" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/ac5e82528b986c4f7cfccbf7764b5d2e824d6175", + "reference": "ac5e82528b986c4f7cfccbf7764b5d2e824d6175", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/type-info": "^7.4.7|^8.0.7" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/cache": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/serializer": "<6.4" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "support": { + "source": "https://github.com/symfony/property-info/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, { "name": "symfony/routing", "version": "v7.4.13", @@ -6641,6 +7552,110 @@ ], "time": "2026-05-24T11:20:33+00:00" }, + { + "name": "symfony/serializer", + "version": "v7.4.14", + "source": { + "type": "git", + "url": "https://github.com/symfony/serializer.git", + "reference": "55acb01b9c8a5211dfbaf68c314d90d0ed2cc3d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/serializer/zipball/55acb01b9c8a5211dfbaf68c314d90d0ed2cc3d1", + "reference": "55acb01b9c8a5211dfbaf68c314d90d0ed2cc3d1", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-php84": "^1.30" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/dependency-injection": "<6.4", + "symfony/property-access": "<6.4.31|>=7.0,<7.4.2|>=8.0,<8.0.2", + "symfony/property-info": "<6.4", + "symfony/type-info": "<7.2.5", + "symfony/uid": "<6.4", + "symfony/validator": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "seld/jsonlint": "^1.10", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^7.2|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/form": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4.31|^7.4.2|^8.0.2", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/type-info": "^7.2.5|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Serializer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/serializer/tree/v7.4.14" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-27T08:31:18+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.7.1", @@ -7001,6 +8016,89 @@ ], "time": "2026-06-05T06:23:12+00:00" }, + { + "name": "symfony/type-info", + "version": "v7.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/type-info.git", + "reference": "cafeedbf157b890e94ac5b83eaed85595106d5d6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/type-info/zipball/cafeedbf157b890e94ac5b83eaed85595106d5d6", + "reference": "cafeedbf157b890e94ac5b83eaed85595106d5d6", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "phpstan/phpdoc-parser": "<1.30" + }, + "require-dev": { + "phpstan/phpdoc-parser": "^1.30|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\TypeInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Arlaud", + "email": "mathias.arlaud@gmail.com" + }, + { + "name": "Baptiste LEDUC", + "email": "baptiste.leduc@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts PHP types information.", + "homepage": "https://symfony.com", + "keywords": [ + "PHPStan", + "phpdoc", + "symfony", + "type" + ], + "support": { + "source": "https://github.com/symfony/type-info/tree/v7.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-22T15:21:55+00:00" + }, { "name": "symfony/uid", "version": "v7.4.9", @@ -7379,6 +8477,229 @@ ], "time": "2026-04-26T05:33:54+00:00" }, + { + "name": "web-auth/cose-lib", + "version": "4.6.0", + "source": { + "type": "git", + "url": "https://github.com/web-auth/cose-lib.git", + "reference": "3afe04df137baf97c5c3e28c5ee6f05536405148" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/3afe04df137baf97c5c3e28c5ee6f05536405148", + "reference": "3afe04df137baf97c5c3e28c5ee6f05536405148", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17|^0.18", + "ext-json": "*", + "ext-openssl": "*", + "php": ">=8.1", + "spomky-labs/pki-framework": "^1.0" + }, + "require-dev": { + "spomky-labs/cbor-php": "^3.2.2" + }, + "suggest": { + "ext-bcmath": "For better performance, please install either GMP (recommended) or BCMath extension", + "ext-gmp": "For better performance, please install either GMP (recommended) or BCMath extension", + "spomky-labs/cbor-php": "For COSE Signature support" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cose\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-auth/cose/contributors" + } + ], + "description": "CBOR Object Signing and Encryption (COSE) For PHP", + "homepage": "https://github.com/web-auth", + "keywords": [ + "COSE", + "RFC8152" + ], + "support": { + "issues": "https://github.com/web-auth/cose-lib/issues", + "source": "https://github.com/web-auth/cose-lib/tree/4.6.0" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2026-07-16T10:19:49+00:00" + }, + { + "name": "web-auth/webauthn-lib", + "version": "5.3.5", + "source": { + "type": "git", + "url": "https://github.com/web-auth/webauthn-lib.git", + "reference": "9e0986d999f4102e24ac8a598d3a80d98b56c19f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/9e0986d999f4102e24ac8a598d3a80d98b56c19f", + "reference": "9e0986d999f4102e24ac8a598d3a80d98b56c19f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-openssl": "*", + "paragonie/constant_time_encoding": "^2.6|^3.0", + "php": ">=8.2", + "phpdocumentor/reflection-docblock": "^5.3|^6.0", + "psr/clock": "^1.0", + "psr/event-dispatcher": "^1.0", + "psr/log": "^1.0|^2.0|^3.0", + "spomky-labs/cbor-php": "^3.0", + "spomky-labs/pki-framework": "^1.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^3.2", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "web-auth/cose-lib": "^4.2.3" + }, + "suggest": { + "psr/log-implementation": "Recommended to receive logs from the library", + "symfony/event-dispatcher": "Recommended to use dispatched events", + "web-token/jwt-library": "Mandatory for fetching Metadata Statement from distant sources" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/web-auth/webauthn-framework", + "name": "web-auth/webauthn-framework" + } + }, + "autoload": { + "psr-4": { + "Webauthn\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-auth/webauthn-library/contributors" + } + ], + "description": "FIDO2/Webauthn Support For PHP", + "homepage": "https://github.com/web-auth", + "keywords": [ + "FIDO2", + "fido", + "webauthn" + ], + "support": { + "source": "https://github.com/web-auth/webauthn-lib/tree/5.3.5" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2026-05-31T15:00:08+00:00" + }, + { + "name": "webmozart/assert", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/2ccb7c2e821038c03a3e6e1700c570c158c55f70", + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^8.2" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" + }, + "type": "library", + "extra": { + "psalm": { + "pluginClass": "Webmozart\\Assert\\PsalmPlugin" + }, + "branch-alias": { + "dev-master": "2.0-dev", + "dev-feature/2-0": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/2.4.1" + }, + "time": "2026-06-15T15:31:57+00:00" + }, { "name": "wire-elements/modal", "version": "3.0.4", @@ -7445,6 +8766,241 @@ } ], "packages-dev": [ + { + "name": "brianium/paratest", + "version": "v7.20.0", + "source": { + "type": "git", + "url": "https://github.com/paratestphp/paratest.git", + "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/81c80677c9ec0ed4ef16b246167f11dec81a6e3d", + "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-simplexml": "*", + "fidry/cpu-core-counter": "^1.3.0", + "jean85/pretty-package-versions": "^2.1.1", + "php": "~8.3.0 || ~8.4.0 || ~8.5.0", + "phpunit/php-code-coverage": "^12.5.3 || ^13.0.1", + "phpunit/php-file-iterator": "^6.0.1 || ^7", + "phpunit/php-timer": "^8 || ^9", + "phpunit/phpunit": "^12.5.14 || ^13.0.5", + "sebastian/environment": "^8.0.3 || ^9", + "symfony/console": "^7.4.7 || ^8.0.7", + "symfony/process": "^7.4.5 || ^8.0.5" + }, + "require-dev": { + "doctrine/coding-standard": "^14.0.0", + "ext-pcntl": "*", + "ext-pcov": "*", + "ext-posix": "*", + "phpstan/phpstan": "^2.1.44", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "symfony/filesystem": "^7.4.6 || ^8.0.6" + }, + "bin": [ + "bin/paratest", + "bin/paratest_for_phpstorm" + ], + "type": "library", + "autoload": { + "psr-4": { + "ParaTest\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Scaturro", + "email": "scaturrob@gmail.com", + "role": "Developer" + }, + { + "name": "Filippo Tessarotto", + "email": "zoeslam@gmail.com", + "role": "Developer" + } + ], + "description": "Parallel testing for PHP", + "homepage": "https://github.com/paratestphp/paratest", + "keywords": [ + "concurrent", + "parallel", + "phpunit", + "testing" + ], + "support": { + "issues": "https://github.com/paratestphp/paratest/issues", + "source": "https://github.com/paratestphp/paratest/tree/v7.20.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/Slamdunk", + "type": "github" + }, + { + "url": "https://paypal.me/filippotessarotto", + "type": "paypal" + } + ], + "time": "2026-03-29T15:46:14+00:00" + }, + { + "name": "composer/pcre", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<2.2.2" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-06-07T11:47:49+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, { "name": "fakerphp/faker", "version": "v1.24.1", @@ -7508,6 +9064,67 @@ }, "time": "2024-11-21T13:46:39+00:00" }, + { + "name": "fidry/cpu-core-counter", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-08-14T07:29:31+00:00" + }, { "name": "filp/whoops", "version": "2.18.4", @@ -7630,6 +9247,66 @@ }, "time": "2025-04-30T06:54:44+00:00" }, + { + "name": "jean85/pretty-package-versions", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", + "vimeo/psalm": "^4.3 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" + }, + "time": "2025-03-19T14:43:43+00:00" + }, { "name": "laravel/agent-detector", "version": "v2.0.2", @@ -8164,6 +9841,463 @@ ], "time": "2026-07-15T19:09:14+00:00" }, + { + "name": "pestphp/pest", + "version": "v4.7.5", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest.git", + "reference": "5dc49a71d63602a9b98fed0f2017c4679ef9f8e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest/zipball/5dc49a71d63602a9b98fed0f2017c4679ef9f8e0", + "reference": "5dc49a71d63602a9b98fed0f2017c4679ef9f8e0", + "shasum": "" + }, + "require": { + "brianium/paratest": "^7.20.0", + "composer/xdebug-handler": "^3.0.5", + "nunomaduro/collision": "^8.9.4", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest-plugin": "^4.0.0", + "pestphp/pest-plugin-arch": "^4.0.2", + "pestphp/pest-plugin-mutate": "^4.0.1", + "pestphp/pest-plugin-profanity": "^4.2.1", + "php": "^8.3.0", + "phpunit/phpunit": "^12.5.30", + "symfony/process": "^7.4.13|^8.1.0" + }, + "conflict": { + "filp/whoops": "<2.18.3", + "phpunit/phpunit": ">12.5.30", + "sebastian/exporter": "<7.0.0", + "webmozart/assert": "<1.11.0" + }, + "require-dev": { + "mrpunyapal/peststan": "^0.2.11", + "pestphp/pest-dev-tools": "^4.1.0", + "pestphp/pest-plugin-browser": "^4.3.1", + "pestphp/pest-plugin-type-coverage": "^4.0.4", + "psy/psysh": "^0.12.24" + }, + "bin": [ + "bin/pest" + ], + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Mutate\\Plugins\\Mutate", + "Pest\\Plugins\\Configuration", + "Pest\\Plugins\\Bail", + "Pest\\Plugins\\Cache", + "Pest\\Plugins\\Coverage", + "Pest\\Plugins\\Init", + "Pest\\Plugins\\Environment", + "Pest\\Plugins\\Help", + "Pest\\Plugins\\Memory", + "Pest\\Plugins\\Only", + "Pest\\Plugins\\Printer", + "Pest\\Plugins\\ProcessIsolation", + "Pest\\Plugins\\Profile", + "Pest\\Plugins\\Retry", + "Pest\\Plugins\\Snapshot", + "Pest\\Plugins\\Verbose", + "Pest\\Plugins\\Version", + "Pest\\Plugins\\Shard", + "Pest\\Plugins\\Tia", + "Pest\\Plugins\\Parallel" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "files": [ + "src/Functions.php", + "src/Pest.php" + ], + "psr-4": { + "Pest\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "The elegant PHP Testing Framework.", + "keywords": [ + "framework", + "pest", + "php", + "test", + "testing", + "unit" + ], + "support": { + "issues": "https://github.com/pestphp/pest/issues", + "source": "https://github.com/pestphp/pest/tree/v4.7.5" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2026-07-06T17:06:29+00:00" + }, + { + "name": "pestphp/pest-plugin", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin.git", + "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/9d4b93d7f73d3f9c3189bb22c220fef271cdf568", + "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0.0", + "composer-runtime-api": "^2.2.2", + "php": "^8.3" + }, + "conflict": { + "pestphp/pest": "<4.0.0" + }, + "require-dev": { + "composer/composer": "^2.8.10", + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Pest\\Plugin\\Manager" + }, + "autoload": { + "psr-4": { + "Pest\\Plugin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest plugin manager", + "keywords": [ + "framework", + "manager", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin/tree/v4.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2025-08-20T12:35:58+00:00" + }, + { + "name": "pestphp/pest-plugin-arch", + "version": "v4.0.2", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-arch.git", + "reference": "3fb0d02a91b9da504b139dc7ab2a31efb7c3215c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/3fb0d02a91b9da504b139dc7ab2a31efb7c3215c", + "reference": "3fb0d02a91b9da504b139dc7ab2a31efb7c3215c", + "shasum": "" + }, + "require": { + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3", + "ta-tikoma/phpunit-architecture-test": "^0.8.7" + }, + "require-dev": { + "pestphp/pest": "^4.4.6", + "pestphp/pest-dev-tools": "^4.1.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Arch\\Plugin" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Arch\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Arch plugin for Pest PHP.", + "keywords": [ + "arch", + "architecture", + "framework", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v4.0.2" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2026-04-10T17:20:19+00:00" + }, + { + "name": "pestphp/pest-plugin-livewire", + "version": "v4.1.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-livewire.git", + "reference": "0b5a137ec6ceadd19dd2c59b9b60039d64f6b4d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-livewire/zipball/0b5a137ec6ceadd19dd2c59b9b60039d64f6b4d2", + "reference": "0b5a137ec6ceadd19dd2c59b9b60039d64f6b4d2", + "shasum": "" + }, + "require": { + "livewire/livewire": "^3.7.4|^4.0.1", + "pestphp/pest": "^4.3.1", + "php": "^8.3" + }, + "require-dev": { + "orchestra/testbench": "^10.9.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Livewire\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest Livewire Plugin", + "keywords": [ + "framework", + "livewire", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-livewire/tree/v4.1.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-01-16T00:56:22+00:00" + }, + { + "name": "pestphp/pest-plugin-mutate", + "version": "v4.0.1", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-mutate.git", + "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/d9b32b60b2385e1688a68cc227594738ec26d96c", + "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.6.1", + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3", + "psr/simple-cache": "^3.0.0" + }, + "require-dev": { + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0", + "pestphp/pest-plugin-type-coverage": "^4.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Pest\\Mutate\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + }, + { + "name": "Sandro Gehri", + "email": "sandrogehri@gmail.com" + } + ], + "description": "Mutates your code to find untested cases", + "keywords": [ + "framework", + "mutate", + "mutation", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v4.0.1" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/gehrisandro", + "type": "github" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2025-08-21T20:19:25+00:00" + }, + { + "name": "pestphp/pest-plugin-profanity", + "version": "v4.2.1", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-profanity.git", + "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/343cfa6f3564b7e35df0ebb77b7fa97039f72b27", + "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27", + "shasum": "" + }, + "require": { + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3" + }, + "require-dev": { + "faissaloux/pest-plugin-inside": "^1.9", + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Profanity\\Plugin" + ] + } + }, + "autoload": { + "psr-4": { + "Pest\\Profanity\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest Profanity Plugin", + "keywords": [ + "framework", + "pest", + "php", + "plugin", + "profanity", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v4.2.1" + }, + "time": "2025-12-08T00:13:17+00:00" + }, { "name": "phar-io/manifest", "version": "2.0.4", @@ -8629,24 +10763,24 @@ }, { "name": "phpunit/phpunit", - "version": "12.5.31", + "version": "12.5.30", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "0608d157a284f15cc73b99a3327eff06b66a176d" + "reference": "900400a5b616d6fb306f9549f6da33ba615d3fbb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0608d157a284f15cc73b99a3327eff06b66a176d", - "reference": "0608d157a284f15cc73b99a3327eff06b66a176d", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/900400a5b616d6fb306f9549f6da33ba615d3fbb", + "reference": "900400a5b616d6fb306f9549f6da33ba615d3fbb", "shasum": "" }, "require": { "ext-dom": "*", - "ext-filter": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", + "ext-xml": "*", "ext-xmlwriter": "*", "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", @@ -8707,7 +10841,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.31" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.30" }, "funding": [ { @@ -8715,7 +10849,7 @@ "type": "other" } ], - "time": "2026-07-06T14:54:16+00:00" + "time": "2026-06-15T13:12:30+00:00" }, { "name": "sebastian/cli-parser", @@ -9678,6 +11812,65 @@ ], "time": "2024-10-20T05:08:20+00:00" }, + { + "name": "ta-tikoma/phpunit-architecture-test", + "version": "0.8.7", + "source": { + "type": "git", + "url": "https://github.com/ta-tikoma/phpunit-architecture-test.git", + "reference": "1248f3f506ca9641d4f68cebcd538fa489754db8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/1248f3f506ca9641d4f68cebcd538fa489754db8", + "reference": "1248f3f506ca9641d4f68cebcd538fa489754db8", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18.0 || ^5.0.0", + "php": "^8.1.0", + "phpdocumentor/reflection-docblock": "^5.3.0 || ^6.0.0", + "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0 || ^13.0.0", + "symfony/finder": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "require-dev": { + "laravel/pint": "^1.13.7", + "phpstan/phpstan": "^1.10.52" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPUnit\\Architecture\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ni Shi", + "email": "futik0ma011@gmail.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Methods for testing application architecture", + "keywords": [ + "architecture", + "phpunit", + "stucture", + "test", + "testing" + ], + "support": { + "issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues", + "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.7" + }, + "time": "2026-02-17T17:25:14+00:00" + }, { "name": "theseer/tokenizer", "version": "2.0.1", diff --git a/config/fortify.php b/config/fortify.php new file mode 100644 index 0000000..5b88b03 --- /dev/null +++ b/config/fortify.php @@ -0,0 +1,183 @@ + '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', + 'passkeys' => 'passkeys', + ], + + /* + |-------------------------------------------------------------------------- + | 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. + | + */ + + // Pages are full-page class-based Livewire components mapped in routes/web.php + // (R1/R2); Fortify registers only its action (POST) routes, not view routes. + 'views' => false, + + /* + |-------------------------------------------------------------------------- + | Passkeys + |-------------------------------------------------------------------------- + | + | These settings configure Fortify's passkey (WebAuthn) support. Passkeys + | allow users to sign in without needing to remember credentials since + | they use public-key cryptography - making them immune to breaches. + | + */ + + 'passkeys' => [ + 'relying_party_id' => parse_url(config('app.url'), PHP_URL_HOST), + 'allowed_origins' => [config('app.url')], + 'timeout' => 60000, + ], + + /* + |-------------------------------------------------------------------------- + | 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' => [ + // v1 customer portal: login + TOTP two-factor only. Public registration, + // password reset, profile/password management and passkeys are out of + // v1 scope (design handoff §2) — enable later as separate screens. + // Features::registration(), + // Features::resetPasswords(), + // Features::emailVerification(), + // Features::updateProfileInformation(), + // Features::updatePasswords(), + Features::twoFactorAuthentication([ + 'confirm' => true, + 'confirmPassword' => true, + // 'window' => 0, + ]), + // Features::passkeys([...]), + ], + +]; diff --git a/database/migrations/2026_07_24_224530_add_two_factor_columns_to_users_table.php b/database/migrations/2026_07_24_224530_add_two_factor_columns_to_users_table.php new file mode 100644 index 0000000..45739ef --- /dev/null +++ b/database/migrations/2026_07_24_224530_add_two_factor_columns_to_users_table.php @@ -0,0 +1,42 @@ +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/database/migrations/2026_07_24_224531_create_passkeys_table.php b/database/migrations/2026_07_24_224531_create_passkeys_table.php new file mode 100644 index 0000000..a0b9e7d --- /dev/null +++ b/database/migrations/2026_07_24_224531_create_passkeys_table.php @@ -0,0 +1,35 @@ +id(); + $table->foreignIdFor(Passkeys::userModel(), 'user_id')->constrained()->cascadeOnDelete(); + $table->string('name'); + $table->string('credential_id')->unique(); + $table->json('credential'); + $table->timestamp('last_used_at')->nullable(); + $table->timestamps(); + + $table->index('user_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('passkeys'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 6b901f8..aa250aa 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -3,23 +3,27 @@ namespace Database\Seeders; use App\Models\User; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\Hash; class DatabaseSeeder extends Seeder { - use WithoutModelEvents; - /** * Seed the application's database. */ public function run(): void { - // User::factory(10)->create(); + // Never create a known-credential account outside local/testing. + if (! app()->environment('local', 'testing')) { + return; + } - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', - ]); + User::updateOrCreate( + ['email' => env('SEED_ADMIN_EMAIL', 'admin@clupilot.local')], + [ + 'name' => 'Admin', + 'password' => Hash::make(env('SEED_ADMIN_PASSWORD', 'password')), + ], + ); } } diff --git a/lang/de/auth.php b/lang/de/auth.php new file mode 100644 index 0000000..ab64d80 --- /dev/null +++ b/lang/de/auth.php @@ -0,0 +1,26 @@ + 'Diese Zugangsdaten stimmen nicht mit unseren Daten überein.', + 'password' => 'Das angegebene Passwort ist nicht korrekt.', + 'throttle' => 'Zu viele Anmeldeversuche. Bitte in :seconds Sekunden erneut versuchen.', + + // Login + 'login_title' => 'Bei CluPilot anmelden', + 'login_subtitle' => 'Melden Sie sich mit Ihren Zugangsdaten an.', + 'email' => 'E-Mail', + 'password_label' => 'Passwort', + 'remember' => 'Angemeldet bleiben', + 'sign_in' => 'Anmelden', + + // Two-factor + 'twofa_title' => 'Zwei-Faktor-Bestätigung', + 'twofa_subtitle' => 'Geben Sie den Code aus Ihrer Authenticator-App ein.', + 'twofa_recovery_subtitle' => 'Geben Sie einen Ihrer Wiederherstellungscodes ein.', + 'otp_digit' => 'Ziffer', + 'recovery_code' => 'Wiederherstellungscode', + 'verify' => 'Bestätigen', + 'use_recovery' => 'Wiederherstellungscode verwenden', + 'use_otp' => 'Authenticator-Code verwenden', +]; diff --git a/lang/de/dashboard.php b/lang/de/dashboard.php new file mode 100644 index 0000000..63512e5 --- /dev/null +++ b/lang/de/dashboard.php @@ -0,0 +1,60 @@ + 'Übersicht', + 'subtitle' => 'Status Ihrer Cloud auf einen Blick.', + + 'nav' => [ + 'overview' => 'Übersicht', + 'cloud' => 'Meine Cloud', + 'users' => 'Nutzer', + 'backups' => 'Backups', + 'invoices' => 'Rechnungen', + 'support' => 'Support', + ], + + 'open_nav' => 'Navigation öffnen', + 'close_nav' => 'Navigation schließen', + 'logout' => 'Abmelden', + + 'kpi' => [ + 'storage' => 'Speicher', + 'users' => 'Aktive Nutzer', + 'backup' => 'Letztes Backup', + 'uptime' => 'Verfügbarkeit', + ], + + 'instance' => 'Instanz', + 'open_cloud' => 'Cloud öffnen', + 'provisioning' => 'Bereitstellung', + 'activity' => 'Aktivität', + 'today_time' => 'Heute :time', + 'yesterday' => 'Gestern', + + 'status' => [ + 'active' => 'Aktiv', + 'provisioning' => 'Bereitstellung', + 'suspended' => 'Ausgesetzt', + 'failed' => 'Fehlgeschlagen', + ], + + 'steps' => [ + 'validate' => 'Bestellung geprüft', + 'provision' => 'Instanz erstellt', + 'configure' => 'Cloud konfiguriert', + 'acceptance' => 'Abnahme bestanden', + ], + + 'step_state' => [ + 'pending' => 'Ausstehend', + 'running' => 'Läuft', + 'done' => 'Abgeschlossen', + 'failed' => 'Fehlgeschlagen', + ], + + 'activity_items' => [ + 'backup_ok' => 'Backup erfolgreich abgeschlossen', + 'user_added' => 'Neuer Nutzer hinzugefügt', + 'login' => 'Anmeldung am Portal', + ], +]; diff --git a/lang/en/auth.php b/lang/en/auth.php new file mode 100644 index 0000000..97a603f --- /dev/null +++ b/lang/en/auth.php @@ -0,0 +1,26 @@ + 'These credentials do not match our records.', + 'password' => 'The provided password is incorrect.', + 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', + + // Login + 'login_title' => 'Sign in to CluPilot', + 'login_subtitle' => 'Sign in with your credentials.', + 'email' => 'Email', + 'password_label' => 'Password', + 'remember' => 'Stay signed in', + 'sign_in' => 'Sign in', + + // Two-factor + 'twofa_title' => 'Two-factor confirmation', + 'twofa_subtitle' => 'Enter the code from your authenticator app.', + 'twofa_recovery_subtitle' => 'Enter one of your recovery codes.', + 'otp_digit' => 'Digit', + 'recovery_code' => 'Recovery code', + 'verify' => 'Verify', + 'use_recovery' => 'Use a recovery code', + 'use_otp' => 'Use authenticator code', +]; diff --git a/lang/en/dashboard.php b/lang/en/dashboard.php new file mode 100644 index 0000000..bc70a59 --- /dev/null +++ b/lang/en/dashboard.php @@ -0,0 +1,60 @@ + 'Overview', + 'subtitle' => 'Your cloud status at a glance.', + + 'nav' => [ + 'overview' => 'Overview', + 'cloud' => 'My cloud', + 'users' => 'Users', + 'backups' => 'Backups', + 'invoices' => 'Invoices', + 'support' => 'Support', + ], + + 'open_nav' => 'Open navigation', + 'close_nav' => 'Close navigation', + 'logout' => 'Sign out', + + 'kpi' => [ + 'storage' => 'Storage', + 'users' => 'Active users', + 'backup' => 'Last backup', + 'uptime' => 'Uptime', + ], + + 'instance' => 'Instance', + 'open_cloud' => 'Open cloud', + 'provisioning' => 'Provisioning', + 'activity' => 'Activity', + 'today_time' => 'Today :time', + 'yesterday' => 'Yesterday', + + 'status' => [ + 'active' => 'Active', + 'provisioning' => 'Provisioning', + 'suspended' => 'Suspended', + 'failed' => 'Failed', + ], + + 'steps' => [ + 'validate' => 'Order validated', + 'provision' => 'Instance created', + 'configure' => 'Cloud configured', + 'acceptance' => 'Acceptance passed', + ], + + 'step_state' => [ + 'pending' => 'Pending', + 'running' => 'Running', + 'done' => 'Done', + 'failed' => 'Failed', + ], + + 'activity_items' => [ + 'backup_ok' => 'Backup completed successfully', + 'user_added' => 'New user added', + 'login' => 'Portal sign-in', + ], +]; diff --git a/phpunit.xml b/phpunit.xml index e7f0a48..1df375b 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -18,17 +18,19 @@ - + + - - - - - - - - - + + + + + + + + + diff --git a/resources/css/app.css b/resources/css/app.css index 9a81e0d..48805b3 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -40,6 +40,11 @@ .font-mono { font-variant-numeric: tabular-nums; } + + /* Alpine: hide elements until initialised. */ + [x-cloak] { + display: none !important; + } } @media (prefers-reduced-motion: reduce) { diff --git a/resources/js/app.js b/resources/js/app.js index c5ffd7d..6a56c80 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,9 +1,49 @@ -// - -/** - * Echo exposes an expressive API for subscribing to channels and listening - * for events that are broadcast by Laravel. Echo and event broadcasting - * allow your team to quickly build robust real-time web applications. - */ - import './echo'; + +// Segmented OTP input behaviour (used by ). Alpine ships with +// Livewire; register on alpine:init. +document.addEventListener('alpine:init', () => { + window.Alpine.data('otpInput', ({ length = 6 } = {}) => ({ + length, + digits: Array(length).fill(''), + + get value() { + return this.digits.join(''); + }, + + cells() { + return this.$root.querySelectorAll('input[inputmode="numeric"]'); + }, + + focusCell(i) { + const cell = this.cells()[i]; + if (cell) cell.focus(); + }, + + onInput(i, e) { + const digit = e.target.value.replace(/\D/g, '').slice(-1); + this.digits[i] = digit; + if (digit && i < this.length - 1) this.focusCell(i + 1); + this.maybeSubmit(); + }, + + onBackspace(i) { + if (!this.digits[i] && i > 0) this.focusCell(i - 1); + }, + + onPaste(e) { + const text = (e.clipboardData.getData('text') || '').replace(/\D/g, '').slice(0, this.length); + for (let i = 0; i < this.length; i++) this.digits[i] = text[i] || ''; + this.focusCell(Math.min(text.length, this.length - 1)); + this.maybeSubmit(); + }, + + maybeSubmit() { + if (this.value.length === this.length) { + // Wait for Alpine to flush :value into the hidden input, otherwise + // requestSubmit() can post a stale/short code. + this.$nextTick(() => this.$root.closest('form')?.requestSubmit()); + } + }, + })); +}); diff --git a/resources/views/components/ui/alert.blade.php b/resources/views/components/ui/alert.blade.php new file mode 100644 index 0000000..7d54139 --- /dev/null +++ b/resources/views/components/ui/alert.blade.php @@ -0,0 +1,12 @@ +@props(['variant' => 'info']) +@php + $variants = [ + 'info' => 'bg-info-bg border-info-border text-info', + 'success' => 'bg-success-bg border-success-border text-success', + 'warning' => 'bg-warning-bg border-warning-border text-warning', + 'danger' => 'bg-danger-bg border-danger-border text-danger', + ]; +@endphp +
merge(['class' => 'rounded border px-3 py-2 text-sm '.($variants[$variant] ?? $variants['info'])]) }}> + {{ $slot }} +
diff --git a/resources/views/components/ui/badge.blade.php b/resources/views/components/ui/badge.blade.php new file mode 100644 index 0000000..097dde9 --- /dev/null +++ b/resources/views/components/ui/badge.blade.php @@ -0,0 +1,28 @@ +@props([ + // status → semantic tone. Always carries a text label (never colour-only, R9/§9). + 'status' => 'info', +]) +@php + $tones = [ + 'active' => 'success', + 'online' => 'success', + 'provisioning' => 'info', + 'pending' => 'info', + 'suspended' => 'warning', + 'warning' => 'warning', + 'failed' => 'danger', + 'offline' => 'danger', + 'info' => 'info', + ]; + $tone = $tones[$status] ?? 'info'; + $classes = [ + 'success' => 'bg-success-bg border-success-border text-success', + 'warning' => 'bg-warning-bg border-warning-border text-warning', + 'danger' => 'bg-danger-bg border-danger-border text-danger', + 'info' => 'bg-info-bg border-info-border text-info', + ][$tone]; +@endphp +merge(['class' => 'inline-flex items-center gap-1.5 rounded-pill border px-2.5 py-0.5 text-xs font-medium '.$classes]) }}> + + {{ $slot }} + diff --git a/resources/views/components/ui/button.blade.php b/resources/views/components/ui/button.blade.php new file mode 100644 index 0000000..af60a40 --- /dev/null +++ b/resources/views/components/ui/button.blade.php @@ -0,0 +1,39 @@ +@props([ + 'variant' => 'primary', + 'size' => 'md', + 'type' => 'button', + 'loading' => false, +]) +@php + $base = 'inline-flex items-center justify-center gap-2 rounded font-semibold transition select-none disabled:opacity-50 disabled:pointer-events-none'; + + $sizes = [ + 'sm' => 'px-3 py-1.5 text-sm', + 'md' => 'px-4 py-2.5 text-sm', + ]; + + // Accent buttons use the AA-safe darker fill (#c2560a) so white text passes + // WCAG AA; #f97316 alone is only ~2.8:1 (design handoff §6.1). + $variants = [ + 'primary' => 'bg-accent-active text-on-accent hover:bg-accent-press', + 'secondary' => 'border border-line-strong bg-surface text-body hover:bg-surface-hover', + 'ghost' => 'text-body hover:bg-surface-hover', + 'danger' => 'bg-danger text-on-accent hover:opacity-90', + ]; + + $classes = $base.' '.($sizes[$size] ?? $sizes['md']).' '.($variants[$variant] ?? $variants['primary']); +@endphp + diff --git a/resources/views/components/ui/card.blade.php b/resources/views/components/ui/card.blade.php new file mode 100644 index 0000000..f795217 --- /dev/null +++ b/resources/views/components/ui/card.blade.php @@ -0,0 +1,14 @@ +@props([ + 'title' => null, + 'padding' => 'p-6', +]) +
merge(['class' => 'rounded-lg border border-line bg-surface shadow-xs']) }}> + @if ($title) +
+

{{ $title }}

+
+ @endif +
+ {{ $slot }} +
+
diff --git a/resources/views/components/ui/checkbox.blade.php b/resources/views/components/ui/checkbox.blade.php new file mode 100644 index 0000000..6002850 --- /dev/null +++ b/resources/views/components/ui/checkbox.blade.php @@ -0,0 +1,15 @@ +@props([ + 'name', + 'label' => null, +]) +@php $id = $attributes->get('id', $name); @endphp + diff --git a/resources/views/components/ui/icon.blade.php b/resources/views/components/ui/icon.blade.php new file mode 100644 index 0000000..74e7081 --- /dev/null +++ b/resources/views/components/ui/icon.blade.php @@ -0,0 +1,25 @@ +@props(['name']) +@php + // Lucide (https://lucide.dev) path bodies — trusted, static markup. + $icons = [ + 'gauge' => '', + 'cloud' => '', + 'users' => '', + 'database' => '', + 'receipt' => '', + 'life-buoy' => '', + 'menu' => '', + 'log-out' => '', + 'chevron-down' => '', + 'external-link'=> '', + 'shield-check' => '', + ]; + $body = $icons[$name] ?? ''; +@endphp + diff --git a/resources/views/components/ui/input.blade.php b/resources/views/components/ui/input.blade.php new file mode 100644 index 0000000..6b87243 --- /dev/null +++ b/resources/views/components/ui/input.blade.php @@ -0,0 +1,36 @@ +@props([ + 'name', + 'label' => null, + 'type' => 'text', + 'hint' => null, +]) +@php + // $errors is shared on web requests; default it so the component also renders + // standalone (e.g. in isolated Blade::render tests). + $errors ??= new \Illuminate\Support\ViewErrorBag; + $id = $attributes->get('id', $name); + $hasError = $errors->has($name); + $field = 'block w-full rounded border bg-surface px-3 py-2 text-sm text-ink placeholder:text-faint transition ' + .($hasError ? 'border-danger bg-danger-bg' : 'border-line'); +@endphp +
+ @if ($label) + + @endif + + merge(['class' => $field]) }} + > + + @if ($hint && ! $hasError) +

{{ $hint }}

+ @endif + + @error($name) +

{{ $message }}

+ @enderror +
diff --git a/resources/views/components/ui/nav-item.blade.php b/resources/views/components/ui/nav-item.blade.php new file mode 100644 index 0000000..25484f0 --- /dev/null +++ b/resources/views/components/ui/nav-item.blade.php @@ -0,0 +1,23 @@ +@props([ + 'href' => '#', + 'active' => false, + 'disabled' => false, +]) +@php + $base = 'flex items-center gap-3 rounded border-l-2 px-3 py-2 text-sm font-medium min-h-11'; + $state = $active + ? 'bg-accent-subtle text-accent-text border-accent' + : 'text-muted border-transparent hover:bg-surface-hover hover:text-body'; +@endphp +@if ($disabled) + {{-- Section not built yet: render a non-interactive item, never a dead link. --}} + merge(['class' => $base.' border-transparent text-faint cursor-not-allowed']) }}> + @isset($icon){{ $icon }}@endisset + {{ $slot }} + +@else + merge(['class' => $base.' '.$state]) }}> + @isset($icon){{ $icon }}@endisset + {{ $slot }} + +@endif diff --git a/resources/views/components/ui/otp-input.blade.php b/resources/views/components/ui/otp-input.blade.php new file mode 100644 index 0000000..31f7af1 --- /dev/null +++ b/resources/views/components/ui/otp-input.blade.php @@ -0,0 +1,30 @@ +@props([ + 'name' => 'code', + 'length' => 6, + 'label' => null, +]) +{{-- Segmented one-time-code input. Alpine keeps the visible segments; a hidden + field carries the joined value for the native form POST. --}} +
+ + +
diff --git a/resources/views/components/ui/progress-stepper.blade.php b/resources/views/components/ui/progress-stepper.blade.php new file mode 100644 index 0000000..417cd66 --- /dev/null +++ b/resources/views/components/ui/progress-stepper.blade.php @@ -0,0 +1,31 @@ +@props([ + // array of ['label' => string, 'state' => pending|running|done|failed] + 'steps' => [], +]) +
    + @foreach ($steps as $i => $step) + @php + $state = $step['state'] ?? 'pending'; + $isLast = $loop->last; + $dot = [ + 'done' => 'bg-success border-success', + 'running' => 'bg-info border-info animate-pulse', + 'failed' => 'bg-danger border-danger', + 'pending' => 'bg-surface border-line-strong', + ][$state] ?? 'bg-surface border-line-strong'; + $labelTone = $state === 'pending' ? 'text-muted' : 'text-ink'; + @endphp +
  1. +
    + + @unless ($isLast) + + @endunless +
    +
    +

    {{ $step['label'] }}

    +

    {{ __('dashboard.step_state.'.$state) }}

    +
    +
  2. + @endforeach +
diff --git a/resources/views/components/ui/stat-tile.blade.php b/resources/views/components/ui/stat-tile.blade.php new file mode 100644 index 0000000..44a5c1d --- /dev/null +++ b/resources/views/components/ui/stat-tile.blade.php @@ -0,0 +1,29 @@ +@props([ + 'label', + 'value', + 'unit' => null, + 'percent' => null, // 0..100 → renders a bar + 'tone' => 'accent', // accent | success | warning | danger +]) +@php + $bar = [ + 'accent' => 'bg-accent', + 'success' => 'bg-success', + 'warning' => 'bg-warning', + 'danger' => 'bg-danger', + ][$tone] ?? 'bg-accent'; + $pct = is_null($percent) ? null : max(0, min(100, (int) $percent)); +@endphp +
merge(['class' => 'rounded-lg border border-line bg-surface p-4 shadow-xs']) }}> +

{{ $label }}

+

+ {{ $value }} + @if ($unit){{ $unit }}@endif +

+ @if (! is_null($pct)) +
+ {{-- the one allowed inline style: dynamic progress width (R4) --}} +
+
+ @endif +
diff --git a/resources/views/layouts/portal-app.blade.php b/resources/views/layouts/portal-app.blade.php new file mode 100644 index 0000000..3c93ebf --- /dev/null +++ b/resources/views/layouts/portal-app.blade.php @@ -0,0 +1,90 @@ + + + + + + + {{ $title ?? config('app.name') }} + @vite(['resources/css/app.css', 'resources/js/app.js']) + @livewireStyles + + +
+ {{-- Sidebar: fixed drawer on small screens, static on large (R7). --}} + + + {{-- Mobile backdrop --}} +
+ +
+
+ +
+ + @auth +
+ +
+
+ @csrf + +
+
+
+ @endauth +
+ +
+ {{ $slot }} +
+
+
+ + @livewireScripts + + diff --git a/resources/views/livewire/auth/login.blade.php b/resources/views/livewire/auth/login.blade.php new file mode 100644 index 0000000..434e39c --- /dev/null +++ b/resources/views/livewire/auth/login.blade.php @@ -0,0 +1,41 @@ +
+
+
+ CluPilot +
+ + +

{{ __('auth.login_title') }}

+

{{ __('auth.login_subtitle') }}

+ + @if (session('status')) + {{ session('status') }} + @endif + +
+ @csrf + + + + + + {{ __('auth.sign_in') }} + + +
+
+
diff --git a/resources/views/livewire/auth/two-factor-challenge.blade.php b/resources/views/livewire/auth/two-factor-challenge.blade.php new file mode 100644 index 0000000..208037a --- /dev/null +++ b/resources/views/livewire/auth/two-factor-challenge.blade.php @@ -0,0 +1,39 @@ +
+
+
+ CluPilot +
+ + +

{{ __('auth.twofa_title') }}

+

{{ __('auth.twofa_subtitle') }}

+

{{ __('auth.twofa_recovery_subtitle') }}

+ + @error('code') + {{ $message }} + @enderror + @error('recovery_code') + {{ $message }} + @enderror + + {{-- TOTP code --}} +
+ @csrf + + {{ __('auth.verify') }} + + + {{-- Recovery code fallback --}} +
+ @csrf + + {{ __('auth.verify') }} + + + +
+
+
diff --git a/resources/views/livewire/dashboard.blade.php b/resources/views/livewire/dashboard.blade.php new file mode 100644 index 0000000..e7f54ba --- /dev/null +++ b/resources/views/livewire/dashboard.blade.php @@ -0,0 +1,61 @@ +
+
+

{{ __('dashboard.title') }}

+

{{ __('dashboard.subtitle') }}

+
+ + {{-- KPI row: 1 → 2 → 4 (R7) --}} +
+ + + + +
+ +
+ {{-- Instance card --}} + +
+
+

{{ __('dashboard.instance') }}

+

{{ $instanceUrl }}

+
+ {{ __('dashboard.status.'.$instanceStatus) }} +
+
+ + + {{ __('dashboard.open_cloud') }} + +
+
+ + {{-- Provisioning progress (live via Reverb once the engine lands) --}} + + + +
+ + {{-- Activity --}} + +
    + @foreach ($activity as $item) +
  • + {{ $item['label'] }} + {{ $item['time'] }} +
  • + @endforeach +
+
+
diff --git a/routes/web.php b/routes/web.php index 86a06c5..fddba75 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,7 +1,19 @@ view('welcome'))->name('home'); + +// Guest auth pages — full-page class-based Livewire components (R1/R2). Fortify +// handles the POST actions (login.store, two-factor.login.store) with views off. +Route::middleware('guest')->group(function () { + Route::get('/login', Login::class)->name('login'); + Route::get('/two-factor-challenge', TwoFactorChallenge::class)->name('two-factor.login'); +}); + +Route::middleware('auth')->group(function () { + Route::get('/dashboard', Dashboard::class)->name('dashboard'); }); diff --git a/tests/Feature/Auth/LoginTest.php b/tests/Feature/Auth/LoginTest.php new file mode 100644 index 0000000..e5ccffc --- /dev/null +++ b/tests/Feature/Auth/LoginTest.php @@ -0,0 +1,52 @@ +withSession(['_token' => 'test-token']) + ->post('/login', array_merge(['_token' => 'test-token'], $data)); +} + +it('renders the login page', function () { + $this->get('/login') + ->assertOk() + ->assertSee('CluPilot') + ->assertSee('name="email"', false); +}); + +it('logs in with valid credentials and redirects to the dashboard', function () { + $user = User::factory()->create(['password' => Hash::make('secret-password')]); + + loginPost(['email' => $user->email, 'password' => 'secret-password']) + ->assertRedirect('/dashboard'); + + $this->assertAuthenticatedAs($user); +}); + +it('rejects invalid credentials', function () { + $user = User::factory()->create(['password' => Hash::make('secret-password')]); + + loginPost(['email' => $user->email, 'password' => 'wrong-password']) + ->assertSessionHasErrors('email'); + + $this->assertGuest(); +}); + +it('throttles repeated failed attempts', function () { + $user = User::factory()->create(['password' => Hash::make('secret-password')]); + + // Exceed the 5/min login limiter. + foreach (range(1, 6) as $ignored) { + loginPost(['email' => $user->email, 'password' => 'wrong-password']); + } + + // Once throttled, even correct credentials are refused. + loginPost(['email' => $user->email, 'password' => 'secret-password']); + + $this->assertGuest(); +}); diff --git a/tests/Feature/Auth/TwoFactorChallengeTest.php b/tests/Feature/Auth/TwoFactorChallengeTest.php new file mode 100644 index 0000000..080ccd2 --- /dev/null +++ b/tests/Feature/Auth/TwoFactorChallengeTest.php @@ -0,0 +1,8 @@ +get('/two-factor-challenge') + ->assertOk() + ->assertSee('CluPilot') + ->assertSee('name="code"', false); +}); diff --git a/tests/Feature/ComponentTest.php b/tests/Feature/ComponentTest.php new file mode 100644 index 0000000..574d16c --- /dev/null +++ b/tests/Feature/ComponentTest.php @@ -0,0 +1,30 @@ +Go'); + + expect($html)->toContain('bg-accent-active')->toContain('Go'); +}); + +it('renders an input with a linked label and error region', function () { + $html = Blade::render(''); + + expect($html) + ->toContain('for="email"') + ->toContain('name="email"') + ->toContain('E-Mail'); +}); + +it('maps badge status to a semantic tone', function () { + $html = Blade::render('Aktiv'); + + expect($html)->toContain('text-success')->toContain('Aktiv'); +}); + +it('renders a stat tile with a progress bar', function () { + $html = Blade::render(''); + + expect($html)->toContain('Speicher')->toContain('width: 69%'); +}); diff --git a/tests/Feature/DashboardTest.php b/tests/Feature/DashboardTest.php new file mode 100644 index 0000000..f5fbc2d --- /dev/null +++ b/tests/Feature/DashboardTest.php @@ -0,0 +1,16 @@ +get('/dashboard')->assertRedirect('/login'); +}); + +it('shows the dashboard to authenticated users', function () { + $user = User::factory()->create(); + + $this->actingAs($user)->get('/dashboard') + ->assertOk() + ->assertSee('CluPilot') + ->assertSee(__('dashboard.title')); +}); diff --git a/tests/Feature/WelcomeTest.php b/tests/Feature/WelcomeTest.php new file mode 100644 index 0000000..8945b4a --- /dev/null +++ b/tests/Feature/WelcomeTest.php @@ -0,0 +1,8 @@ +get('/') + ->assertOk() + ->assertSee('CluPilot') + ->assertSee(route('login'), false); +}); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..941e024 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,50 @@ +extend(TestCase::class) + ->use(RefreshDatabase::class) + ->in('Feature'); + +/* +|-------------------------------------------------------------------------- +| Expectations +|-------------------------------------------------------------------------- +| +| When you're writing tests, you often need to check that values meet certain conditions. The +| "expect()" function gives you access to a set of "expectations" methods that you can use +| to assert different things. Of course, you may extend the Expectation API at any time. +| +*/ + +expect()->extend('toBeOne', function () { + return $this->toBe(1); +}); + +/* +|-------------------------------------------------------------------------- +| Functions +|-------------------------------------------------------------------------- +| +| While Pest is very powerful out-of-the-box, you may have some testing code specific to your +| project that you don't want to repeat in every file. Here you can also expose helpers as +| global functions to help you to reduce the number of lines of code in your test files. +| +*/ + +function something() +{ + // .. +}