diff --git a/app/Filament/Pages/Auth/EditProfile.php b/app/Filament/Pages/Auth/EditProfile.php index 3a09dce197..f79f5b5c9d 100644 --- a/app/Filament/Pages/Auth/EditProfile.php +++ b/app/Filament/Pages/Auth/EditProfile.php @@ -39,6 +39,7 @@ use Filament\Schemas\Components\Tabs; use Filament\Schemas\Components\Tabs\Tab; use Filament\Schemas\Components\Utilities\Get; +use Filament\Schemas\Components\View; use Filament\Schemas\Schema; use Filament\Support\Colors\Color; use Filament\Support\Enums\Width; @@ -234,6 +235,16 @@ protected function getDefaultTabs(): array ->map(fn (MultiFactorAuthenticationProvider $multiFactorAuthenticationProvider) => Group::make($multiFactorAuthenticationProvider->getManagementSchemaComponents()) ->statePath($multiFactorAuthenticationProvider->getId())) ->all()), + Tab::make('passkeys') + ->label(trans('profile.tabs.passkeys')) + ->icon(TablerIcon::Fingerprint) + ->schema([ + Section::make(trans('profile.tabs.passkeys')) + ->description(trans('passkeys.description')) + ->schema([ + View::make('passkeys.livewire.passkeys-tab'), + ]), + ]), Tab::make('api_keys') ->label(trans('profile.tabs.api_keys')) ->icon(TablerIcon::Key) diff --git a/app/Filament/Pages/Auth/Login.php b/app/Filament/Pages/Auth/Login.php index 58dbb1bbeb..43b20579ae 100644 --- a/app/Filament/Pages/Auth/Login.php +++ b/app/Filament/Pages/Auth/Login.php @@ -9,6 +9,7 @@ use Filament\Forms\Components\TextInput; use Filament\Schemas\Components\Actions; use Filament\Schemas\Components\Component; +use Filament\Schemas\Components\View; use Filament\Schemas\Schema; use Filament\Support\Colors\Color; use Illuminate\Validation\ValidationException; @@ -25,6 +26,16 @@ public function boot(OAuthService $oauthService, CaptchaService $captchaService) $this->captchaService = $captchaService; } + public function content(Schema $schema): Schema + { + return $schema + ->components([ + $this->getFormContentComponent(), + $this->getMultiFactorChallengeFormContentComponent(), + View::make('passkeys.login'), + ]); + } + public function form(Schema $schema): Schema { $components = [ diff --git a/app/Livewire/Passkeys.php b/app/Livewire/Passkeys.php new file mode 100644 index 0000000000..58674f77ce --- /dev/null +++ b/app/Livewire/Passkeys.php @@ -0,0 +1,61 @@ +mountAction('deleteAction', ['passkey' => $passkeyId]); + } + + public function deleteAction(): Action + { + return Action::make('deleteAction') + ->label(trans('passkeys.delete')) + ->color('danger') + ->requiresConfirmation() + ->action(fn (array $arguments) => $this->deletePasskey((int) $arguments['passkey'])); + } + + public function deletePasskey(int $passkeyId): void + { + $this->currentUser()->passkeys()->findOrFail($passkeyId); + + parent::deletePasskey($passkeyId); + + Notification::make() + ->title(trans('passkeys.deleted_notification_title')) + ->success() + ->send(); + } + + public function storePasskey(string $passkey): void + { + parent::storePasskey($passkey); + + Notification::make() + ->title(trans('passkeys.created_notification_title')) + ->success() + ->send(); + } + + public function render(): View + { + return view('passkeys.livewire.passkeys', data: [ + 'passkeys' => $this->currentUser()->passkeys()->get(), + ]); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 57b42d45ae..c8e3ce90ec 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -43,6 +43,8 @@ use Illuminate\Support\Str; use Illuminate\Validation\Rules\In; use ResourceBundle; +use Spatie\LaravelPasskeys\Models\Concerns\HasPasskeys; +use Spatie\LaravelPasskeys\Models\Concerns\InteractsWithPasskeys; use Spatie\Permission\Traits\HasRoles; /** @@ -94,7 +96,7 @@ * @method static Builder|User whereUsername($value) * @method static Builder|User whereUuid($value) */ -class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, FilamentUser, HasAppAuthentication, HasAppAuthenticationRecovery, HasAvatar, HasEmailAuthentication, HasName, HasTenants, Validatable +class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, FilamentUser, HasAppAuthentication, HasAppAuthenticationRecovery, HasAvatar, HasEmailAuthentication, HasName, HasPasskeys, HasTenants, Validatable { use Authenticatable; use Authorizable { can as protected canned; } @@ -103,6 +105,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac use HasFactory; use HasRoles; use HasValidation { getRules as getValidationRules; } + use InteractsWithPasskeys; use Notifiable; public const USER_LEVEL_USER = 0; @@ -504,4 +507,14 @@ public function toggleEmailAuthentication(bool $condition): void { $this->update(['mfa_email_enabled' => $condition]); } + + public function getPasskeyDisplayName(): string + { + return $this->username ?? $this->email; + } + + public function getPasskeyUserId(): int + { + return $this->id; + } } diff --git a/app/Providers/Filament/FilamentServiceProvider.php b/app/Providers/Filament/FilamentServiceProvider.php index 5b30646aa2..a3934d4133 100644 --- a/app/Providers/Filament/FilamentServiceProvider.php +++ b/app/Providers/Filament/FilamentServiceProvider.php @@ -4,6 +4,7 @@ use App\Enums\CustomizationKey; use App\Enums\TablerIcon; +use App\Livewire\Passkeys; use Filament\Actions\Action; use Filament\Actions\CreateAction; use Filament\Actions\DeleteAction; @@ -257,6 +258,7 @@ public function boot(): void SchemaIconAlias::COMPONENTS_WIZARD_COMPLETED_STEP => TablerIcon::Check, ]); + Livewire::component('filament-passkeys', Passkeys::class); } public function register(): void {} diff --git a/composer.json b/composer.json index b29ac9b5f8..9bf6c42eba 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "filament/filament": "^4.5", "gboquizosanchez/filament-log-viewer": "^2.1", "guzzlehttp/guzzle": "^7.10", - "laravel/framework": "^12.49", + "laravel/framework": "~12.49.0", "laravel/helpers": "^1.8", "laravel/sanctum": "^4.2", "laravel/socialite": "^5.24", @@ -35,6 +35,7 @@ "spatie/laravel-data": "^4.19", "spatie/laravel-fractal": "^6.3", "spatie/laravel-health": "^1.34", + "spatie/laravel-passkeys": "^1.5", "spatie/laravel-permission": "^6.24", "spatie/laravel-query-builder": "^6.4", "spatie/temporary-directory": "^2.3", diff --git a/composer.lock b/composer.lock index 1a99ca1133..940abdc933 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ee0e729079d522f013d1203a624a4765", + "content-hash": "dc1eedb66ac1930be9401bf0b299c2e5", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -7809,6 +7809,84 @@ ], "time": "2025-07-17T15:46:43+00:00" }, + { + "name": "spatie/laravel-passkeys", + "version": "1.5.3", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-passkeys.git", + "reference": "641217418497ffa9fdb6c2f5f67a1332b58a6012" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-passkeys/zipball/641217418497ffa9fdb6c2f5f67a1332b58a6012", + "reference": "641217418497ffa9fdb6c2f5f67a1332b58a6012", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^11.0|^12.0", + "php": "^8.2|^8.3|^8.4", + "spatie/laravel-package-tools": "^1.16", + "web-auth/webauthn-lib": "^5.0" + }, + "require-dev": { + "larastan/larastan": "^3.4", + "laravel/pint": "^1.14", + "livewire/livewire": "^3.5", + "nunomaduro/collision": "^8.1.1", + "orchestra/testbench": "^10.0", + "pestphp/pest": "^3.0", + "pestphp/pest-plugin-arch": "^3.0", + "pestphp/pest-plugin-laravel": "^3.0", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "spatie/laravel-ray": "^1.35" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\LaravelPasskeys\\LaravelPasskeysServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\LaravelPasskeys\\": "src/", + "Spatie\\LaravelPasskeys\\Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Use passkeys in your Laravel app", + "homepage": "https://github.com/spatie/laravel-passkeys", + "keywords": [ + "laravel", + "laravel-passkeys", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-passkeys/issues", + "source": "https://github.com/spatie/laravel-passkeys/tree/1.5.3" + }, + "funding": [ + { + "url": "https://github.com/Spatie", + "type": "github" + } + ], + "time": "2026-01-12T08:33:46+00:00" + }, { "name": "spatie/laravel-permission", "version": "6.24.0", @@ -8236,6 +8314,186 @@ ], "time": "2026-01-12T07:42:22+00:00" }, + { + "name": "spomky-labs/cbor-php", + "version": "3.2.2", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/cbor-php.git", + "reference": "2a5fb86aacfe1004611370ead6caa2bfc88435d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/2a5fb86aacfe1004611370ead6caa2bfc88435d0", + "reference": "2a5fb86aacfe1004611370ead6caa2bfc88435d0", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14", + "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.2.2" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2025-11-13T13:00:34+00:00" + }, + { + "name": "spomky-labs/pki-framework", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/pki-framework.git", + "reference": "f0e9a548df4e3942886adc9b7830581a46334631" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/f0e9a548df4e3942886adc9b7830581a46334631", + "reference": "f0e9a548df4e3942886adc9b7830581a46334631", + "shasum": "" + }, + "require": { + "brick/math": "^0.10|^0.11|^0.12|^0.13|^0.14", + "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" + }, + "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.4.1" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2025-12-20T12:57:40+00:00" + }, { "name": "symfony/clock", "version": "v7.4.0", @@ -10598,40 +10856,31 @@ "time": "2026-01-26T15:07:59+00:00" }, { - "name": "symfony/routing", + "name": "symfony/property-access", "version": "v7.4.4", "source": { "type": "git", - "url": "https://github.com/symfony/routing.git", - "reference": "0798827fe2c79caeed41d70b680c2c3507d10147" + "url": "https://github.com/symfony/property-access.git", + "reference": "fa49bf1ca8fce1ba0e2dba4e4658554cfb9364b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/0798827fe2c79caeed41d70b680c2c3507d10147", - "reference": "0798827fe2c79caeed41d70b680c2c3507d10147", + "url": "https://api.github.com/repos/symfony/property-access/zipball/fa49bf1ca8fce1ba0e2dba4e4658554cfb9364b1", + "reference": "fa49bf1ca8fce1ba0e2dba4e4658554cfb9364b1", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3" - }, - "conflict": { - "symfony/config": "<6.4", - "symfony/dependency-injection": "<6.4", - "symfony/yaml": "<6.4" + "symfony/property-info": "^6.4.32|~7.3.10|^7.4.4|^8.0.4" }, "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/expression-language": "^6.4|^7.0|^8.0", - "symfony/http-foundation": "^6.4|^7.0|^8.0", - "symfony/yaml": "^6.4|^7.0|^8.0" + "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\\Routing\\": "" + "Symfony\\Component\\PropertyAccess\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -10651,16 +10900,21 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Maps an HTTP request to a set of configuration variables", + "description": "Provides functions to read and write from/to an object or array using a simple string notation", "homepage": "https://symfony.com", "keywords": [ - "router", - "routing", - "uri", - "url" + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.4.4" + "source": "https://github.com/symfony/property-access/tree/v7.4.4" }, "funding": [ { @@ -10680,46 +10934,49 @@ "type": "tidelift" } ], - "time": "2026-01-12T12:19:02+00:00" + "time": "2026-01-05T08:47:25+00:00" }, { - "name": "symfony/service-contracts", - "version": "v3.6.1", + "name": "symfony/property-info", + "version": "v7.4.5", "source": { "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + "url": "https://github.com/symfony/property-info.git", + "reference": "1c9d326bd69602561e2ea467a16c09b5972eee21" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "url": "https://api.github.com/repos/symfony/property-info/zipball/1c9d326bd69602561e2ea467a16c09b5972eee21", + "reference": "1c9d326bd69602561e2ea467a16c09b5972eee21", "shasum": "" }, "require": { - "php": ">=8.1", - "psr/container": "^1.1|^2.0", - "symfony/deprecation-contracts": "^2.5|^3" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/type-info": "~7.3.10|^7.4.4|^8.0.4" }, "conflict": { - "ext-psr": "<1.1|>=2" + "phpdocumentor/reflection-docblock": "<5.2|>=6", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/cache": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/serializer": "<6.4" }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.6-dev" - } + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2", + "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\\Contracts\\Service\\": "" + "Symfony\\Component\\PropertyInfo\\": "" }, "exclude-from-classmap": [ - "/Test/" + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -10728,26 +10985,26 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to writing services", + "description": "Extracts information about PHP class' properties using metadata of popular sources", "homepage": "https://symfony.com", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + "source": "https://github.com/symfony/property-info/tree/v7.4.5" }, "funding": [ { @@ -10767,41 +11024,316 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:30:57+00:00" + "time": "2026-01-27T16:16:02+00:00" }, { - "name": "symfony/string", + "name": "symfony/routing", "version": "v7.4.4", "source": { "type": "git", - "url": "https://github.com/symfony/string.git", - "reference": "1c4b10461bf2ec27537b5f36105337262f5f5d6f" + "url": "https://github.com/symfony/routing.git", + "reference": "0798827fe2c79caeed41d70b680c2c3507d10147" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/1c4b10461bf2ec27537b5f36105337262f5f5d6f", - "reference": "1c4b10461bf2ec27537b5f36105337262f5f5d6f", + "url": "https://api.github.com/repos/symfony/routing/zipball/0798827fe2c79caeed41d70b680c2c3507d10147", + "reference": "0798827fe2c79caeed41d70b680c2c3507d10147", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.33", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { - "symfony/translation-contracts": "<2.5" + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" }, "require-dev": { - "symfony/emoji": "^7.1|^8.0", - "symfony/http-client": "^6.4|^7.0|^8.0", - "symfony/intl": "^6.4|^7.0|^8.0", - "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0|^8.0" - }, - "type": "library", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "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": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.4.4" + }, + "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-01-12T12:19:02+00:00" + }, + { + "name": "symfony/serializer", + "version": "v7.4.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/serializer.git", + "reference": "480cd1237c98ab1219c20945b92c9d4480a44f47" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/serializer/zipball/480cd1237c98ab1219c20945b92c9d4480a44f47", + "reference": "480cd1237c98ab1219c20945b92c9d4480a44f47", + "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|>=6", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/dependency-injection": "<6.4", + "symfony/property-access": "<6.4", + "symfony/property-info": "<6.4", + "symfony/uid": "<6.4", + "symfony/validator": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2", + "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|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/type-info": "^7.1.8|^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.5" + }, + "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-01-27T08:59:58+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "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": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/string", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "1c4b10461bf2ec27537b5f36105337262f5f5d6f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/1c4b10461bf2ec27537b5f36105337262f5f5d6f", + "reference": "1c4b10461bf2ec27537b5f36105337262f5f5d6f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "type": "library", "autoload": { "files": [ "Resources/functions.php" @@ -11042,6 +11574,89 @@ ], "time": "2025-07-15T13:41:35+00:00" }, + { + "name": "symfony/type-info", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/type-info.git", + "reference": "f83c725e72b39b2704b9d6fc85070ad6ac7a5889" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/type-info/zipball/f83c725e72b39b2704b9d6fc85070ad6ac7a5889", + "reference": "f83c725e72b39b2704b9d6fc85070ad6ac7a5889", + "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.4" + }, + "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-01-09T12:14:21+00:00" + }, { "name": "symfony/uid", "version": "v7.4.4", @@ -11565,6 +12180,163 @@ ], "time": "2024-11-21T01:49:47+00:00" }, + { + "name": "web-auth/cose-lib", + "version": "4.5.0", + "source": { + "type": "git", + "url": "https://github.com/web-auth/cose-lib.git", + "reference": "5adac6fe126994a3ee17ed9950efb4947ab132a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/5adac6fe126994a3ee17ed9950efb4947ab132a9", + "reference": "5adac6fe126994a3ee17ed9950efb4947ab132a9", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14", + "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.5.0" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2026-01-03T14:43:18+00:00" + }, + { + "name": "web-auth/webauthn-lib", + "version": "5.2.3", + "source": { + "type": "git", + "url": "https://github.com/web-auth/webauthn-lib.git", + "reference": "8782f575032fedc36e2eb27c39c736054e2b6867" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/8782f575032fedc36e2eb27c39c736054e2b6867", + "reference": "8782f575032fedc36e2eb27c39c736054e2b6867", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-openssl": "*", + "paragonie/constant_time_encoding": "^2.6|^3.0", + "php": ">=8.2", + "phpdocumentor/reflection-docblock": "^5.3", + "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", + "symfony/deprecation-contracts": "^3.2", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.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.2.3" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2025-12-20T10:54:02+00:00" + }, { "name": "webmozart/assert", "version": "1.11.0", @@ -15470,5 +16242,5 @@ "platform-overrides": { "php": "8.2" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } diff --git a/config/passkeys.php b/config/passkeys.php new file mode 100644 index 0000000000..bf6940f5d0 --- /dev/null +++ b/config/passkeys.php @@ -0,0 +1,41 @@ + '/', + + /* + * These class are responsible for performing core tasks regarding passkeys. + * You can customize them by creating a class that extends the default, and + * by specifying your custom class name here. + */ + 'actions' => [ + 'generate_passkey_register_options' => Spatie\LaravelPasskeys\Actions\GeneratePasskeyRegisterOptionsAction::class, + 'store_passkey' => Spatie\LaravelPasskeys\Actions\StorePasskeyAction::class, + 'generate_passkey_authentication_options' => \Spatie\LaravelPasskeys\Actions\GeneratePasskeyAuthenticationOptionsAction::class, + 'find_passkey' => Spatie\LaravelPasskeys\Actions\FindPasskeyToAuthenticateAction::class, + 'configure_ceremony_step_manager_factory' => Spatie\LaravelPasskeys\Actions\ConfigureCeremonyStepManagerFactoryAction::class, + ], + + /* + * These properties will be used to generate the passkey. + */ + 'relying_party' => [ + 'name' => config('app.name'), + 'id' => parse_url(config('app.url'), PHP_URL_HOST), + 'icon' => null, + ], + + /* + * The models used by the package. + * + * You can override this by specifying your own models + */ + 'models' => [ + 'passkey' => Spatie\LaravelPasskeys\Models\Passkey::class, + 'authenticatable' => env('AUTH_MODEL', App\Models\User::class), + ], +]; diff --git a/database/migrations/2026_02_07_103629_create_passkeys_table.php b/database/migrations/2026_02_07_103629_create_passkeys_table.php new file mode 100644 index 0000000000..1b6995d5e8 --- /dev/null +++ b/database/migrations/2026_02_07_103629_create_passkeys_table.php @@ -0,0 +1,37 @@ +getTable(); + + Schema::create('passkeys', function (Blueprint $table) use ($authenticatableTableName) { + $table->id(); + + $table + ->unsignedInteger('authenticatable_id') + ->constrained(table: $authenticatableTableName, indexName: 'passkeys_authenticatable_fk') + ->cascadeOnDelete(); + + $table->text('name'); + $table->text('credential_id'); + $table->json('data'); + + $table->timestamp('last_used_at')->nullable(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('passkeys'); + } +}; diff --git a/lang/en/passkeys.php b/lang/en/passkeys.php new file mode 100644 index 0000000000..0f125d5231 --- /dev/null +++ b/lang/en/passkeys.php @@ -0,0 +1,18 @@ + 'Authenticate using a passkey', + 'create' => 'Create', + 'delete' => 'Delete', + 'error_something_went_wrong_generating_the_passkey' => 'Something went wrong generating the passkey', + 'invalid' => 'Invalid passkey', + 'last_used' => 'Last used', + 'name' => 'Name', + 'name_placeholder' => 'My passkey', + 'no_passkeys_registered' => 'No passkeys registered', + 'not_used_yet' => 'Not used yet', + 'passkeys' => 'Passkeys', + 'description' => 'Passkeys let you log in without needing a password. Instead of a password, you can generate a passkey which will be stored in 1Password, macOS password app, or alternative app on your favourite OS.', + 'created_notification_title' => 'Your passkey has been created', + 'deleted_notification_title' => 'Your passkey has been deleted', +]; diff --git a/lang/en/profile.php b/lang/en/profile.php index db8df82d66..f23b199896 100644 --- a/lang/en/profile.php +++ b/lang/en/profile.php @@ -11,6 +11,7 @@ 'keys' => 'Keys', '2fa' => '2FA', 'customization' => 'Customization', + 'passkeys' => 'Passkeys', ], 'username' => 'Username', 'admin' => 'Admin', diff --git a/package.json b/package.json index 47ca60a886..c76c6503c8 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "vite": "7.1.11" }, "dependencies": { + "@simplewebauthn/browser": "^13.2.2", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-search": "^0.15.0", "@xterm/addon-web-links": "^0.11.0", diff --git a/resources/js/app.js b/resources/js/app.js index e0dc6dc922..fa5c05b141 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -14,4 +14,5 @@ document.body.removeChild(textarea); success ? resolve() : reject('Fallback copy failed'); }); -})(); \ No newline at end of file +})(); +import './passkeys/index.js'; \ No newline at end of file diff --git a/resources/js/passkeys/index.js b/resources/js/passkeys/index.js new file mode 100644 index 0000000000..abec0ad6e5 --- /dev/null +++ b/resources/js/passkeys/index.js @@ -0,0 +1,9 @@ +import { + browserSupportsWebAuthn, + startAuthentication, + startRegistration, +} from '@simplewebauthn/browser' + +window.browserSupportsWebAuthn = browserSupportsWebAuthn; +window.startAuthentication = startAuthentication; +window.startRegistration = startRegistration; diff --git a/resources/views/passkeys/components/authenticate.blade.php b/resources/views/passkeys/components/authenticate.blade.php new file mode 100644 index 0000000000..3af69a17c9 --- /dev/null +++ b/resources/views/passkeys/components/authenticate.blade.php @@ -0,0 +1,17 @@ +
+ @include('passkeys::components.partials.authenticateScript') + +
+ @csrf +
+ + @if($message = session()->get('authenticatePasskey::message')) + @php + \Filament\Notifications\Notification::make() + ->title($message) + ->danger() + ->send(); + session()->forget('authenticatePasskey::message'); + @endphp + @endif +
diff --git a/resources/views/passkeys/livewire/partials/createScript.blade.php b/resources/views/passkeys/livewire/partials/createScript.blade.php new file mode 100644 index 0000000000..4d5a17bb9e --- /dev/null +++ b/resources/views/passkeys/livewire/partials/createScript.blade.php @@ -0,0 +1,11 @@ +@script + +@endscript diff --git a/resources/views/passkeys/livewire/passkeys-tab.blade.php b/resources/views/passkeys/livewire/passkeys-tab.blade.php new file mode 100644 index 0000000000..298db9f604 --- /dev/null +++ b/resources/views/passkeys/livewire/passkeys-tab.blade.php @@ -0,0 +1 @@ + diff --git a/resources/views/passkeys/livewire/passkeys.blade.php b/resources/views/passkeys/livewire/passkeys.blade.php new file mode 100644 index 0000000000..d7a0a12862 --- /dev/null +++ b/resources/views/passkeys/livewire/passkeys.blade.php @@ -0,0 +1,51 @@ +
+
+
+ + + + + @error('name') +

{{ $message }}

+ @enderror +
+ + + {{ trans('passkeys.create') }} + +
+ + @if($passkeys->isNotEmpty()) +
+ {{ trans('passkeys.passkeys') }} +
    + @foreach($passkeys as $passkey) + +
    +
    + {{ $passkey->name }} + {{ trans('passkeys.last_used') }}: {{ $passkey->last_used_at?->diffForHumans() ?? trans('passkeys.not_used_yet') }} +
    + + + {{ trans('passkeys.delete') }} + +
    +
    + @endforeach +
+
+ @endif + + +
+ +@include('passkeys.livewire.partials.createScript') diff --git a/resources/views/passkeys/login.blade.php b/resources/views/passkeys/login.blade.php new file mode 100644 index 0000000000..5716f7094d --- /dev/null +++ b/resources/views/passkeys/login.blade.php @@ -0,0 +1,5 @@ +@include('passkeys.components.authenticate') + + + {{ trans('passkeys.authenticate_using_passkey') }} + diff --git a/resources/views/passkeys/profile.blade.php b/resources/views/passkeys/profile.blade.php new file mode 100644 index 0000000000..7ea7decf0f --- /dev/null +++ b/resources/views/passkeys/profile.blade.php @@ -0,0 +1,11 @@ + + + {{ trans('passkeys.passkeys') }} + + + + {{ trans('passkeys.description') }} + + + + diff --git a/routes/auth.php b/routes/auth.php index 0406b11bde..57841346b6 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -2,6 +2,8 @@ use App\Http\Controllers\Auth\OAuthController; use Illuminate\Support\Facades\Route; +use Spatie\LaravelPasskeys\Http\Controllers\AuthenticateUsingPasskeyController; +use Spatie\LaravelPasskeys\Http\Controllers\GeneratePasskeyAuthenticationOptionsController; Route::redirect('/login', '/login')->name('auth.login'); @@ -9,3 +11,8 @@ Route::get('/redirect/{driver}', [OAuthController::class, 'redirect'])->name('auth.oauth.redirect'); Route::get('/callback/{driver}', [OAuthController::class, 'callback'])->name('auth.oauth.callback')->withoutMiddleware('guest'); }); + +Route::prefix('auth/passkeys')->group(function () { + Route::get('/authentication-options', GeneratePasskeyAuthenticationOptionsController::class)->name('passkeys.authentication_options'); + Route::post('/authenticate', AuthenticateUsingPasskeyController::class)->name('passkeys.login'); +}); diff --git a/yarn.lock b/yarn.lock index 866904019b..0f6faa19a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -292,6 +292,11 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.1.tgz#56eeb602545ec03ce84633b331c2e3ece07b99c3" integrity sha512-rLarc1Ofcs3DHtgSzFO31pZsCh8g05R2azN1q3fF+H423Co87My0R+tazOEvYVKXSLh8C4LerMK41/K7wlklcg== +"@simplewebauthn/browser@^13.2.2": + version "13.2.2" + resolved "https://registry.yarnpkg.com/@simplewebauthn/browser/-/browser-13.2.2.tgz#4cde38c4c6969a039c23c2a3d931ecb69f937910" + integrity sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA== + "@tailwindcss/forms@^0.5.9": version "0.5.10" resolved "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz"