Conversation
📝 WalkthroughWalkthroughThis PR integrates WebAuthn-based passkey authentication into the application using Spatie's Laravel Passkeys package. It adds database migrations, user model updates, authentication routes, a Livewire component for passkey management in the profile, UI integration in the login page, JavaScript WebAuthn helpers, and related configuration and localization files. Changes
Sequence DiagramsequenceDiagram
actor User
participant Browser
participant LoginPage as Login Page
participant AuthController as Auth Controller
participant WebAuthn as WebAuthn API
participant PasskeysDB as Passkeys DB
participant UserDB as User DB
User->>LoginPage: Clicks "Authenticate with Passkey"
LoginPage->>AuthController: GET /auth/passkeys/authentication-options
AuthController->>PasskeysDB: Fetch passkeys for user
AuthController->>LoginPage: Return passkey options
LoginPage->>Browser: Call startAuthentication(options)
Browser->>WebAuthn: Initiate WebAuthn ceremony
WebAuthn->>User: Prompt for passkey
User->>WebAuthn: Verify with passkey
WebAuthn->>Browser: Return signed assertion
Browser->>LoginPage: Receive passkey assertion
LoginPage->>AuthController: POST /auth/passkeys/authenticate (assertion)
AuthController->>PasskeysDB: Verify credential_id and signature
AuthController->>UserDB: Fetch user and establish session
AuthController->>LoginPage: Redirect to dashboard
LoginPage->>User: Login successful
sequenceDiagram
actor User
participant ProfilePage as Profile Page
participant PasskeysUI as Passkeys UI
participant Livewire as Livewire Component
participant WebAuthn as WebAuthn API
participant PasskeysDB as Passkeys DB
User->>ProfilePage: Navigate to Passkeys tab
ProfilePage->>PasskeysUI: Render passkey management form
User->>PasskeysUI: Enter passkey name and click create
PasskeysUI->>Livewire: validatePasskeyProperties()
Livewire->>PasskeysUI: Emit passkeyPropertiesValidated event
PasskeysUI->>WebAuthn: Call startRegistration(passkeyOptions)
WebAuthn->>User: Prompt to register new passkey
User->>WebAuthn: Complete registration ceremony
WebAuthn->>PasskeysUI: Return passkey credential
PasskeysUI->>Livewire: storePasskey(passkey JSON)
Livewire->>PasskeysDB: Store passkey with name and credential_id
Livewire->>User: Send creation notification
PasskeysUI->>PasskeysUI: Refresh passkey list
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This comment was marked as off-topic.
This comment was marked as off-topic.
A passkey is something you can use to log in without the need of writing user and password each time, not sure how it entirely works, just know that. |
This comment was marked as off-topic.
This comment was marked as off-topic.
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Fix all issues with AI agents
In `@app/Models/User.php`:
- Around line 104-109: The trait imports contain duplicates—remove the repeated
HasFactory and Notifiable from the mixed import so only the new trait remains;
specifically, keep the existing use HasFactory; keep the existing use
Notifiable; and change the combined line that currently reads use HasFactory,
InteractsWithPasskeys, Notifiable; to import only InteractsWithPasskeys (i.e.,
use InteractsWithPasskeys;) so trait imports are not duplicated in the User
model.
In `@public/css/marcelweidum/filament-passkeys/filament-passkeys-styles.css`:
- Around line 1-2: The committed compiled stylesheet
public/css/marcelweidum/filament-passkeys/filament-passkeys-styles.css is
orphaned and should be removed or integrated: either delete that file from the
repo (public/css/marcelweidum/filament-passkeys/filament-passkeys-styles.css) if
the marcelweidum/filament-passkeys integration already provides styles via
vendor/filament, or add an import for the stylesheet into the single CSS
entrypoint resources/css/app.css (so Vite builds/serves it) and remove the
compiled public asset to avoid duplication; update any packaging/build config
accordingly.
In `@resources/views/vendor/passkeys/components/authenticate.blade.php`:
- Around line 14-22: Replace the non-interactive <div> that calls
authenticateWithPasskey() with a semantic <button type="button"> so the passkey
action is focusable and keyboard-operable; keep the existing onclick handler
(authenticateWithPasskey()), transfer the CSS classes (e.g., "underline
cursor-pointer") to the button, preserve the conditional slot rendering (use the
same $slot or fallback text {{
__('passkeys::passkeys.authenticate_using_passkey') }}), and add any necessary
ARIA attributes (e.g., aria-label) if the visible text is insufficient for
screen readers.
In
`@resources/views/vendor/passkeys/components/partials/authenticateScript.blade.php`:
- Around line 2-17: The authenticateWithPasskey function lacks error handling
and doesn't check fetch status; wrap the logic in a try/catch inside
authenticateWithPasskey, check response.ok after fetch('{{
route('passkeys.authentication_options') }}') and throw or handle non-OK
statuses before calling response.json(), catch errors from both fetch and
startAuthentication({ optionsJSON: options }) and surface a user-visible message
(e.g., set an error text element or alert) so the user knows what failed; also
ensure you only add the 'formdata' listener to the element with id
'passkey-login-form' once (or remove it after use) to avoid duplicate handlers
when retrying.
In `@resources/views/vendor/passkeys/livewire/partials/createScript.blade.php`:
- Around line 3-8: Wrap the await startRegistration call inside a try/catch in
the Livewire.on('passkeyPropertiesValidated', ...) handler: try to await
startRegistration({ optionsJSON: passkeyOptions }) and on success call
`@this.call`('storePasskey', JSON.stringify(passkey)); in the catch block log the
error (console.error) and invoke a backend/error handler like
`@this.call`('passkeyRegistrationFailed', error.message) (or emit a Livewire
event) so user cancellation or WebAuthn errors are handled gracefully; update
the handler around startRegistration and storePasskey accordingly.
In `@resources/views/vendor/passkeys/livewire/passkeys.blade.php`:
- Around line 6-7: The label's for="name" doesn't match any input id, breaking
accessibility; update the input element with an id attribute that matches the
label (e.g., add id="name" to the <input> that uses wire:model="name") so the
<label for="name"> correctly associates and clicking the label focuses the
input; if there are multiple name fields ensure the id is unique but still
matches the label.
- Around line 31-35: The delete button in the passkeys.blade.php view calls
deletePasskey({{ $passkey->id }}) directly and lacks a confirmation step; update
the button to prompt the user before invoking the Livewire action (e.g., use
Livewire's wire:confirm or a JS confirm) so deletion is not immediate. Modify
the <button> that triggers deletePasskey to include a confirmation
attribute/handler that aborts the call when the user cancels, ensuring the
deletePasskey method (on the Livewire component) only runs after explicit user
confirmation.
🧹 Nitpick comments (3)
database/migrations/2026_02_07_103629_create_passkeys_table.php (1)
8-31: Missingdown()method — rollback will fail.The migration lacks a
down()method, sophp artisan migrate:rollbackwill not undo this table creation. While not a blocker, it's good practice to include one.♻️ Add down() method
}); } + + public function down(): void + { + Schema::dropIfExists('passkeys'); + } };public/js/marcelweidum/filament-passkeys/filament-passkeys-scripts.js (1)
1-2: Vendored minified JS lacks provenance and version tracking.This appears to be a pre-built asset from the
filament-passkeyspackage (wrapping@simplewebauthn/browser). Committing minified vendor JS directly makes it impossible to audit changes, verify integrity, or track the upstream version. Consider:
- Documenting the exact package version this was built from (e.g., in a comment or adjacent
VERSIONfile).- Ideally, relying on
filament:assetsor composer's asset publishing (php artisan vendor:publish) so the asset is reproducible from a known package version rather than a manually committed blob.resources/views/vendor/passkeys/livewire/passkeys.blade.php (1)
1-42: Hardcoded light-mode styles won't work with Filament's dark mode.Classes like
text-gray-700,bg-gray-100,border-gray-300are light-mode-only. Filament supports dark mode, and these elements will be unreadable or visually broken in dark themes. Consider using Filament's built-in component classes or addingdark:variants.
public/css/marcelweidum/filament-passkeys/filament-passkeys-styles.css
Outdated
Show resolved
Hide resolved
resources/views/vendor/passkeys/components/authenticate.blade.php
Outdated
Show resolved
Hide resolved
| async function authenticateWithPasskey(remember = false) { | ||
| const response = await fetch('{{ route('passkeys.authentication_options') }}') | ||
|
|
||
| const options = await response.json(); | ||
|
|
||
| const startAuthenticationResponse = await startAuthentication({ optionsJSON: options, }); | ||
|
|
||
| const form = document.getElementById('passkey-login-form'); | ||
|
|
||
| form.addEventListener('formdata', ({formData}) => { | ||
| formData.set('remember', remember); | ||
| formData.set('start_authentication_response', JSON.stringify(startAuthenticationResponse)); | ||
| }); | ||
|
|
||
| form.submit(); | ||
| } |
There was a problem hiding this comment.
No error handling — user gets no feedback on failure.
Both fetch() and startAuthentication() can fail (network error, user cancels ceremony, server error), but there is no try/catch. An unhandled rejection leaves the user stuck with no indication of what went wrong. Additionally, the fetch response status isn't checked before parsing JSON.
Proposed fix
async function authenticateWithPasskey(remember = false) {
- const response = await fetch('{{ route('passkeys.authentication_options') }}')
-
- const options = await response.json();
-
- const startAuthenticationResponse = await startAuthentication({ optionsJSON: options, });
-
- const form = document.getElementById('passkey-login-form');
-
- form.addEventListener('formdata', ({formData}) => {
- formData.set('remember', remember);
- formData.set('start_authentication_response', JSON.stringify(startAuthenticationResponse));
- });
-
- form.submit();
+ try {
+ const response = await fetch('{{ route('passkeys.authentication_options') }}');
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch authentication options');
+ }
+
+ const options = await response.json();
+
+ const startAuthenticationResponse = await startAuthentication({ optionsJSON: options });
+
+ const form = document.getElementById('passkey-login-form');
+
+ form.addEventListener('formdata', ({formData}) => {
+ formData.set('remember', remember);
+ formData.set('start_authentication_response', JSON.stringify(startAuthenticationResponse));
+ });
+
+ form.submit();
+ } catch (error) {
+ console.error('Passkey authentication failed:', error);
+ alert(error.message || 'Authentication failed. Please try again.');
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function authenticateWithPasskey(remember = false) { | |
| const response = await fetch('{{ route('passkeys.authentication_options') }}') | |
| const options = await response.json(); | |
| const startAuthenticationResponse = await startAuthentication({ optionsJSON: options, }); | |
| const form = document.getElementById('passkey-login-form'); | |
| form.addEventListener('formdata', ({formData}) => { | |
| formData.set('remember', remember); | |
| formData.set('start_authentication_response', JSON.stringify(startAuthenticationResponse)); | |
| }); | |
| form.submit(); | |
| } | |
| async function authenticateWithPasskey(remember = false) { | |
| try { | |
| const response = await fetch('{{ route('passkeys.authentication_options') }}'); | |
| if (!response.ok) { | |
| throw new Error('Failed to fetch authentication options'); | |
| } | |
| const options = await response.json(); | |
| const startAuthenticationResponse = await startAuthentication({ optionsJSON: options }); | |
| const form = document.getElementById('passkey-login-form'); | |
| form.addEventListener('formdata', ({formData}) => { | |
| formData.set('remember', remember); | |
| formData.set('start_authentication_response', JSON.stringify(startAuthenticationResponse)); | |
| }); | |
| form.submit(); | |
| } catch (error) { | |
| console.error('Passkey authentication failed:', error); | |
| alert(error.message || 'Authentication failed. Please try again.'); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
`@resources/views/vendor/passkeys/components/partials/authenticateScript.blade.php`
around lines 2 - 17, The authenticateWithPasskey function lacks error handling
and doesn't check fetch status; wrap the logic in a try/catch inside
authenticateWithPasskey, check response.ok after fetch('{{
route('passkeys.authentication_options') }}') and throw or handle non-OK
statuses before calling response.json(), catch errors from both fetch and
startAuthentication({ optionsJSON: options }) and surface a user-visible message
(e.g., set an error text element or alert) so the user knows what failed; also
ensure you only add the 'formdata' listener to the element with id
'passkey-login-form' once (or remove it after use) to avoid duplicate handlers
when retrying.
resources/views/vendor/passkeys/livewire/partials/createScript.blade.php
Show resolved
Hide resolved
| <label for="name" class="block text-sm font-medium text-gray-700">{{ __('passkeys::passkeys.name') }}</label> | ||
| <input autocomplete="off" type="text" wire:model="name" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"> |
There was a problem hiding this comment.
Label for attribute doesn't match any input id — broken accessibility.
The <label for="name"> won't associate with the <input> because the input is missing id="name". Clicking the label won't focus the input.
Proposed fix
- <input autocomplete="off" type="text" wire:model="name" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
+ <input id="name" autocomplete="off" type="text" wire:model="name" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <label for="name" class="block text-sm font-medium text-gray-700">{{ __('passkeys::passkeys.name') }}</label> | |
| <input autocomplete="off" type="text" wire:model="name" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"> | |
| <label for="name" class="block text-sm font-medium text-gray-700">{{ __('passkeys::passkeys.name') }}</label> | |
| <input id="name" autocomplete="off" type="text" wire:model="name" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"> |
🤖 Prompt for AI Agents
In `@resources/views/vendor/passkeys/livewire/passkeys.blade.php` around lines 6 -
7, The label's for="name" doesn't match any input id, breaking accessibility;
update the input element with an id attribute that matches the label (e.g., add
id="name" to the <input> that uses wire:model="name") so the <label for="name">
correctly associates and clicking the label focuses the input; if there are
multiple name fields ensure the id is unique but still matches the label.
There was a problem hiding this comment.
@lancepioch Thanks for confirming! Adding id="name" to the input will ensure the label association works correctly, improving keyboard navigation and screen reader support for users managing their passkeys.
| <div> | ||
| <button wire:click="deletePasskey({{ $passkey->id }})" class="inline-flex justify-center py-2 px-4 text-sm font-medium text-white bg-red-600"> | ||
| {{ __('passkeys::passkeys.delete') }} | ||
| </button> | ||
| </div> |
There was a problem hiding this comment.
Passkey deletion has no confirmation dialog.
Deleting a passkey is a destructive, irreversible action that could lock a user out of their account if it's their only authentication method. A single misclick on the delete button would immediately remove it. Add a confirmation step (e.g., wire:confirm).
Proposed fix using Livewire's wire:confirm
- <button wire:click="deletePasskey({{ $passkey->id }})" class="inline-flex justify-center py-2 px-4 text-sm font-medium text-white bg-red-600">
+ <button wire:click="deletePasskey({{ $passkey->id }})" wire:confirm="{{ __('passkeys::passkeys.confirm_delete') }}" class="inline-flex justify-center py-2 px-4 text-sm font-medium text-white bg-red-600 rounded-md">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div> | |
| <button wire:click="deletePasskey({{ $passkey->id }})" class="inline-flex justify-center py-2 px-4 text-sm font-medium text-white bg-red-600"> | |
| {{ __('passkeys::passkeys.delete') }} | |
| </button> | |
| </div> | |
| <div> | |
| <button wire:click="deletePasskey({{ $passkey->id }})" wire:confirm="{{ __('passkeys::passkeys.confirm_delete') }}" class="inline-flex justify-center py-2 px-4 text-sm font-medium text-white bg-red-600 rounded-md"> | |
| {{ __('passkeys::passkeys.delete') }} | |
| </button> | |
| </div> |
🤖 Prompt for AI Agents
In `@resources/views/vendor/passkeys/livewire/passkeys.blade.php` around lines 31
- 35, The delete button in the passkeys.blade.php view calls deletePasskey({{
$passkey->id }}) directly and lacks a confirmation step; update the button to
prompt the user before invoking the Livewire action (e.g., use Livewire's
wire:confirm or a JS confirm) so deletion is not immediate. Modify the <button>
that triggers deletePasskey to include a confirmation attribute/handler that
aborts the call when the user cancels, ensuring the deletePasskey method (on the
Livewire component) only runs after explicit user confirmation.
|
As for the other Rabbit comments, I'm not sure if I have to act or leave it this way, as it's how the package does it. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@app/Models/User.php`:
- Around line 516-519: The method getPasskeyUserId() currently returns an int
which violates the HasPasskeys interface (expects string) and will cause a
TypeError; update getPasskeyUserId() to return a string by casting the id to
string (e.g. return (string) $this->id) or, if preferred, return the user's UUID
(e.g. return $this->uuid) so the method signature and return type match the
interface and WebAuthn expectations.
🧹 Nitpick comments (1)
app/Models/User.php (1)
510-514:usernameis required and cannot be null — the??fallback is dead code.Per the validation rules on Line 165,
usernameisrequired. The null-coalesce to$this->emailwill never trigger. Not harmful, but worth noting — if you truly want a fallback for edge cases (e.g., incomplete model hydration), this is fine as defensive coding.
There was a problem hiding this comment.
I'm almost positive this should NOT be committed.
There was a problem hiding this comment.
I'm almost positive this should NOT be committed.
Was making Rabbit suggestions but if you check discussion I need to redo this entirely ig...
There was a problem hiding this comment.
I'm almost positive this should NOT be committed.
| <label for="name" class="block text-sm font-medium text-gray-700">{{ __('passkeys::passkeys.name') }}</label> | ||
| <input autocomplete="off" type="text" wire:model="name" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"> |
Co-authored-by: Lance Pioch <lancepioch@gmail.com>
|
I'm aware of the tests failing, just found that composer upgraded everything when adding the spatie passkeys, I'll be checking how to fix it and push, I miss my server... |
|
The last commit to shut phpstan was not tested entirely, I appreciate any suggestions on that + the pr itself. |
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Fix all issues with AI agents
In `@app/Filament/PasskeysPlugin.php`:
- Line 49: Remove the duplicate Livewire component registration by keeping the
Livewire::component('filament-passkeys', Passkeys::class) call only inside
PasskeysPlugin::boot() and deleting the identical registration in
FilamentServiceProvider; locate the duplicate Livewire::component(...)
invocation in FilamentServiceProvider and remove that line so the component is
registered exclusively by PasskeysPlugin::boot().
In `@app/Livewire/Passkeys.php`:
- Around line 26-33: The Action handler in deleteAction currently forwards
$arguments['passkey'] directly to deletePasskey(int $passkeyId) which can be
re-hydrated as a string under Filament v3/Livewire with strict_types=1; update
the action closure in deleteAction to cast the incoming argument to int (e.g.,
(int) $arguments['passkey']) before calling deletePasskey so the signature of
deletePasskey matches and no TypeError occurs.
- Around line 35-43: The deletePasskey method currently calls
parent::deletePasskey(passkeyId) without verifying ownership, allowing deletion
of other users' passkeys; before calling parent::deletePasskey(int $passkeyId)
validate the authenticated user's ownership by loading the passkey via
auth()->user()->passkeys()->findOrFail($passkeyId) (or equivalent) to ensure the
passkey belongs to the current user, then proceed to call parent::deletePasskey
and send the Notification as before.
In `@config/passkeys.php`:
- Around line 26-30: The relying_party 'id' currently uses
parse_url(config('app.url'), PHP_URL_HOST) which can be null if app.url is
missing or malformed; update config/passkeys.php to compute the host from
parse_url(config('app.url'), PHP_URL_HOST) and if that yields null fall back to
parsing env('APP_URL') and finally to a safe default (e.g. config('app.name') or
request()->getHost()) so the 'relying_party' => ['id' => ...] is never null;
adjust the logic around the relying_party 'id' entry (parse_url,
config('app.url')) to use this fallback chain.
- Line 8: Replace the hardcoded redirect value for 'redirect_to_after_login'
with a Filament URL helper so the post-login redirect adapts to panel path
changes: use Filament::getPanel('app')->getUrl() (or call the appropriate static
helper to obtain the app panel URL) in place of '/' for the
'redirect_to_after_login' config entry; alternatively, if you prefer
user-controlled redirects, update the existing 'redirect_to_admin' preference
handling to feed its value into the same config key so users can choose their
passkey post-login destination.
In `@lang/en/passkeys.php`:
- Around line 5-19: The translation entries (e.g., authenticate_using_passkey,
create, delete, error_something_went_wrong_generating_the_passkey, invalid,
last_used, name, name_placeholder, no_passkeys_registered, not_used_yet,
passkeys, description, created_notification_title, deleted_notification_title)
should be plain string values, not calls to the translator helper; replace each
__('passkeys::passkeys.<key>') expression with the actual translation string for
that key (or the package's published string) so the returned array contains
literal strings and does not invoke __() during file evaluation or caching.
In `@lang/en/profile.php`:
- Line 73: Fix the typo in the translation value for the 'passkeys_description'
key by changing "1Pass" to "1Password", and remove the near‑identical duplicate
by centralizing the string: keep the canonical 'passkeys_description' entry in
one translations file (e.g., the passkeys translations) and have the other file
(the profile translations) reference that same key instead of duplicating the
text so both use a single source of truth.
In `@resources/views/passkeys/livewire/passkeys.blade.php`:
- Line 51: The `@include`('passkeys::livewire.partials.createScript') line in the
passkeys.blade.php view references a non-existent view and an unregistered view
namespace; either remove that include entirely or replace it with the correct
dotted view path (e.g., use the existing dot-notation view name for the partial)
or create the missing partial file at
resources/views/passkeys/livewire/partials/createScript.blade.php so the include
resolves; update the include reference string accordingly wherever it appears.
- Around line 34-40: The deletePasskey method is missing an ownership check and
accepts a raw ID from the frontend, enabling an IDOR; update deletePasskey to
first load the passkey via the current user relationship (e.g.,
$this->currentUser()->passkeys()->findOrFail($passkeyId)) to ensure it belongs
to the authenticated user, then call parent::deletePasskey($passkeyId) (or
delete via the resolved model) so deletion only proceeds for owned passkeys.
🧹 Nitpick comments (3)
app/Filament/Pages/Auth/EditProfile.php (1)
237-248: Consider adding a visibility condition for the passkeys tab.Other tabs like
oauth(line 189) are conditionally visible. The passkeys tab is always shown regardless of configuration. If the passkeys feature should be toggleable (e.g., via config or feature flag), consider adding a->visible(...)clause similar to other conditional tabs.resources/views/passkeys/components/authenticate.blade.php (1)
8-15: Session-based notification in Blade@phpblock works but is unconventional.Triggering
Filament\Notifications\Notification::make()->send()inside a@phpblock in Blade is functional but couples notification logic to the view layer. This is a minor concern — consider moving this to a controller/middleware or Livewire component for cleaner separation. Not blocking.config/passkeys.php (1)
15-21: Inconsistent FQCN formatting: leading backslash on line 18.Line 18 uses
\Spatie\LaravelPasskeys\Actions\...with a leading backslash, while lines 16, 17, 19, and 20 omit it. This is functionally equivalent but inconsistent.Proposed fix
- 'generate_passkey_authentication_options' => \Spatie\LaravelPasskeys\Actions\GeneratePasskeyAuthenticationOptionsAction::class, + 'generate_passkey_authentication_options' => Spatie\LaravelPasskeys\Actions\GeneratePasskeyAuthenticationOptionsAction::class,
lang/en/profile.php
Outdated
| 'no_oauth' => 'No Accounts Linked', | ||
| 'no_api_keys' => 'No API Keys', | ||
| 'no_ssh_keys' => 'No SSH Keys', | ||
| '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 1Pass, MacOS password app, or alternative app on your favourite OS.', |
There was a problem hiding this comment.
Typo: "1Pass" should be "1Password".
Also, this description is duplicated nearly verbatim in lang/en/passkeys.php (line 17). Consider defining it in one place and referencing it from the other to avoid drift.
🤖 Prompt for AI Agents
In `@lang/en/profile.php` at line 73, Fix the typo in the translation value for
the 'passkeys_description' key by changing "1Pass" to "1Password", and remove
the near‑identical duplicate by centralizing the string: keep the canonical
'passkeys_description' entry in one translations file (e.g., the passkeys
translations) and have the other file (the profile translations) reference that
same key instead of duplicating the text so both use a single source of truth.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@resources/views/passkeys/livewire/partials/createScript.blade.php`:
- Around line 1-11: The handler registered in
Livewire.on('passkeyPropertiesValidated', ...) calls startRegistration(...)
without error handling, causing unhandled promise rejections when the user
cancels or the browser lacks passkey support; wrap the await startRegistration
call in a try/catch, on success continue to call `@this.call`('storePasskey',
JSON.stringify(passkey)), and on error call a new Livewire endpoint (e.g.,
`@this.call`('handlePasskeyError', error.message)) or emit a client-side event to
surface a user-friendly message and log the error; also inspect error.name
(e.g., NotAllowedError / NotSupportedError / AbortError) to tailor the message.
🧹 Nitpick comments (2)
app/Filament/Pages/Auth/EditProfile.php (1)
237-248: Consider adding a visibility condition for the passkeys tab.Other tabs like
oauthand2faconditionally show based on feature availability (e.g.,->visible(fn () => Filament::hasMultiFactorAuthentication())). The passkeys tab is always visible regardless of whether thePasskeysPluginis registered on the panel. If passkeys support is optional or can be disabled, guard this tab similarly.Tab::make('passkeys') ->label(trans('profile.tabs.passkeys')) ->icon(TablerIcon::Fingerprint) + ->visible(fn () => filament()->hasPlugin('filament-passkeys')) ->schema([app/Livewire/Passkeys.php (1)
47-55: Consider wrappingstorePasskeyin a try/catch for a friendlier UX.If
parent::storePasskey()throws (e.g., malformed WebAuthn response, duplicate key), the user sees an unhandled error instead of a notification. A try/catch with an error notification would be more consistent with the delete flow's UX.Suggested improvement
public function storePasskey(string $passkey): void { - parent::storePasskey($passkey); - - Notification::make() - ->title(__('passkeys.created_notification_title')) - ->success() - ->send(); + try { + parent::storePasskey($passkey); + + Notification::make() + ->title(__('passkeys.created_notification_title')) + ->success() + ->send(); + } catch (\Throwable $e) { + Notification::make() + ->title(__('passkeys.error_notification_title')) + ->danger() + ->send(); + } }
| ->description(trans('passkeys.description')) | ||
| ->schema([ | ||
| Group::make([ | ||
| view('passkeys.livewire.passkeys-tab'), |
There was a problem hiding this comment.
I don't think you should be adding a view like this. iirc there is a View component from filament.
app/Livewire/Passkeys.php
Outdated
| @@ -0,0 +1,63 @@ | |||
| <?php | |||
|
|
|||
| declare(strict_types=1); | |||
| ); | ||
|
|
||
| FilamentView::registerRenderHook( | ||
| PanelsRenderHook::AUTH_LOGIN_FORM_AFTER, |
There was a problem hiding this comment.
Don't need a render hook for the login form, we already have a custom login page.
Same goes for all the blade views.
| public function subject(...$subjects): self | ||
| { | ||
| foreach (Arr::wrap($subjects) as $subject) { | ||
| foreach ($subjects as $subject) { |
There was a problem hiding this comment.
Nope, phpstan will fail.
https://github.com/pelican-dev/panel/actions/runs/21787248639/job/62860620225
There was a problem hiding this comment.
I was using main the day I made passkeys with the latest changes, then how is it possible to fail?
There was a problem hiding this comment.
You must have done composer upgrade as it updated to v12.50 which is broken.
There was a problem hiding this comment.
You must have done composer upgrade as it updated to v12.50 which is broken.
I didn't, i just made a require to spatie's passkeys.
There was a problem hiding this comment.
Not sure why this needs to be a giant discussion. Require 12.49 , revert this change, and be done with it.
lang/en/passkeys.php
Outdated
| @@ -0,0 +1,20 @@ | |||
| <?php | |||
|
|
|||
| declare(strict_types=1); | |||
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@composer.json`:
- Line 18: The composer.json entry "laravel/framework": "~12.49.0" is too
restrictive and pins Laravel to patch releases only; change this constraint back
to a caret-based range (e.g. "^12.49") so the project can receive minor releases
within Laravel 12.x, and then run composer update to confirm dependency
resolution; if composer fails, identify and fix the underlying conflicting
package (inspect composer why/why-not and check packages that require
illuminate/* such as spatie/laravel-passkeys) rather than keeping the tilde pin.
| "gboquizosanchez/filament-log-viewer": "^2.1", | ||
| "guzzlehttp/guzzle": "^7.10", | ||
| "laravel/framework": "^12.49", | ||
| "laravel/framework": "~12.49.0", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's check the actual composer.json file to confirm the constraint
cat composer.json | head -50Repository: pelican-dev/panel
Length of output: 1973
🌐 Web query:
spatie laravel-passkeys composer requirements
💡 Result:
spatie/laravel-passkeys Composer requirements (current Packagist release):
php: ^8.2 | ^8.3 | ^8.4illuminate/contracts: ^11.0 | ^12.0(i.e., Laravel 11 or 12 components)spatie/laravel-package-tools: ^1.16web-auth/webauthn-lib: ^5.0[1]
Docs note (Spatie): the package “requires PHP 8.4+, Laravel 12+.” [2]
Practical requirement: it includes a Livewire component, so you should have Livewire installed. [3]
Sources: [1] (packagist.org) [2] (spatie.be) [3] (github.com)
Citations:
- 1: https://packagist.org/packages/spatie/laravel-passkeys?utm_source=openai
- 2: https://spatie.be/docs/laravel-passkeys/v1/requirements?utm_source=openai
- 3: https://github.com/spatie/laravel-passkeys?utm_source=openai
Unnecessarily restrictive Laravel version constraint.
Changing from ^12.49 to ~12.49.0 pins Laravel to patch-level updates only (12.49.x), blocking all future minor releases (12.50, 12.51, …). This prevents the project from receiving new features, bug fixes, and security patches shipped in minor versions.
spatie/laravel-passkeys supports illuminate/contracts: ^11.0 | ^12.0, meaning it's compatible with all Laravel 12.x versions—there is no technical reason to restrict Laravel to 12.49.x. This constraint appears to be a workaround for a dependency resolution issue rather than a legitimate compatibility requirement. Investigate and resolve the root cause instead of locking down the version.
Proposed fix: revert to the original caret constraint
- "laravel/framework": "~12.49.0",
+ "laravel/framework": "^12.49",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "laravel/framework": "~12.49.0", | |
| "laravel/framework": "^12.49", |
🤖 Prompt for AI Agents
In `@composer.json` at line 18, The composer.json entry "laravel/framework":
"~12.49.0" is too restrictive and pins Laravel to patch releases only; change
this constraint back to a caret-based range (e.g. "^12.49") so the project can
receive minor releases within Laravel 12.x, and then run composer update to
confirm dependency resolution; if composer fails, identify and fix the
underlying conflicting package (inspect composer why/why-not and check packages
that require illuminate/* such as spatie/laravel-passkeys) rather than keeping
the tilde pin.
| $this->captchaService = $captchaService; | ||
| } | ||
|
|
||
| public function content(Schema $schema): Schema |
There was a problem hiding this comment.
Use the form function that's already there. And remove all the extra views.
There was a problem hiding this comment.
So I was checking this, and I don't seem to find a way to make it how you want, as the view is required for the button to appear.
I would like it if you could give me some kind of direction so I can fix it.
There was a problem hiding this comment.
You extract the components that are used in the views and add them to the form instead, e.g. with Action::make or TextInput::make or whatever is needed.
There was a problem hiding this comment.
You extract the components that are used in the views and add them to the
forminstead, e.g. withAction::makeorTextInput::makeor whatever is needed.
I was checking this, and I don't seem to know how, as each method will always return me to the same point. JS won't call passkeys or if it does, it won't log in.
MarcelWeldium passkeys worked this way, and it requires the views for the actions to work.
It uses a lot of custom JS on a lot of files to do certain things, view is necessary to ensure everything works, as each file imports another view.
There was a problem hiding this comment.
I'll still try to find a way, but I'm not sure about it...
|
@Boy132 |
Right now, only Jesus knows how this works, the added resources are required to work, either we ship them already or we use filament:assets.
I'm still open to changes, but it works, so by the law of a good developer, would be nice to NOT touch it.