Repository navigation
Add CopyInviteLinkAction for manual invite link sharing - #66
Merged
Merged
Conversation
Introduce a record action that generates the same set-password URL as the invite email (shared InviteUrl builder) and shows it in a modal with copy-to-clipboard, without sending a notification. Useful when no mail driver is configured. Email invite remains the default. Co-authored-by: Scott Grayson <scottgrayson@users.noreply.github.com>
Generate the broker token only when the admin clicks Generate link (fillForm no longer mints on modal open); keep the modal open via halt() for copying. Reuse canInviteUser() for action visibility on InviteAction and CopyInviteLinkAction. Add Livewire action tests for visibility, generation, and token invalidation on regenerate. Co-authored-by: Scott Grayson <scottgrayson@users.noreply.github.com>
Co-authored-by: Scott Grayson <scottgrayson@users.noreply.github.com>
Pass invite=1 into getResetPasswordUrl overrides so signed panel routes carry the invite flag. Match filament.*.auth.password-reset.reset in InviteMiddleware, apply filament-invite.expire whenever the invite session is set (including Livewire submit via persistent middleware), and use Filament's auth password broker instead of hard-coding users. Co-authored-by: Scott Grayson <scottgrayson@users.noreply.github.com>
Add FilamentInvitePlugin and attach it to every panel after boot so panel password-reset routes get extended invite expiry without using the web middleware group. Keep web-group registration for non-panel password.reset routes. Prove wiring in the Filament 61-minute test via the panel middleware stack only. Co-authored-by: Scott Grayson <scottgrayson@users.noreply.github.com>
Replace the too-late app.booted auto-attach with afterResolving on PanelRegistry in packageRegistered so InviteMiddleware is on the panel before Filament routes snapshot getMiddleware(). Guard against duplicate registration when apps also call ->plugin(). Update the Filament expiry test to use the boot-registered panel route from AdminPanelProvider. Co-authored-by: Scott Grayson <scottgrayson@users.noreply.github.com>
scottgrayson
marked this pull request as ready for review
October 19, 2026 19:48
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Details
Adds a way for admins to generate and copy the set-password invite URL without sending email — useful when the app has no mail driver configured (e.g. Laravel Cloud without a verified sending domain).
API choice
Separate
CopyInviteLinkAction(record action), rather than an option onInviteAction.InviteAction/InviteBulkActionbehavior is unchanged for the send path.Shared URL building lives in
Tapp\FilamentInvite\Support\InviteUrl, used by bothSetPasswordmail and the copy-link path so the URL (route /getResetPasswordUrl()override,invite=1extended expiry) stays in sync.Explicit generation (avoids silent token invalidation)
Opening the modal does not mint a broker token. Pattern used:
createInviteLink()runs, the schema is filled with the URL, and$action->halt()keeps the modal open so the admin can copy.This is the standard Filament 4/5 “submit → fill schema →
halt()” keep-open pattern (same idea as create-another), rather than minting infillFormon mount.Eligibility
InviteActionandCopyInviteLinkActionboth usehidden(fn (Model $user) => ! $this->canInviteUser($user)).canInviteUser()is the single source of truth (requiresMustVerifyEmail, unverified, authenticated admin withupdateability) and no longer null-derefsauth()->user().Extended expiry for Filament panel reset URLs
Apps using the README
getResetPasswordUrl()override (signedfilament.{panel}.auth.password-reset.reset) were stuck with the default 60-minute broker expiry. Fixed by:InviteUrl::for()passes['invite' => 1]intogetResetPasswordUrl($token, $parameters)so the signed panel URL includes the invite flag.InviteMiddlewarematches bothpassword.resetandfilament.*.auth.password-reset.reset. When the invite session is set, it extends the broker expire tofilament-invite.expireon that request (including Livewire form submits via persistent middleware). Brokers are cleared after updating config so the new expire is used.Filament::getAuthPasswordBroker()(falling back toauth.defaults.passwords), not a hard-codeduserskey.Panel middleware wiring
Filament panels use their own middleware stack, not the
webgroup. Auto-attach usesafterResolving(PanelRegistry::class)inpackageRegistered()(notapp.booted):PanelProviderhas fully configured the panel (soInviteMiddlewareis appended afterStartSession).getMiddleware()and snapshot the stack.FilamentInvitePlugin::registerOnPanel()is a no-op when the plugin is already present (safe if the app also calls->plugin(FilamentInvitePlugin::make())).webgroup for non-panelpassword.resetroutes.The Filament 61-minute test uses an
AdminPanelProviderregistered like a real app (before boot) and asserts the boot-registered panel reset route already includesInviteMiddleware(and notweb).Usage
Tests
SetPassword/getResetPasswordUrl().password.resetinvite; normal reset still expires at 60 minutes.Local: Pest 19 passed, Pint clean, PHPStan level 4 clean. No version bump.