Skip to content

Add CopyInviteLinkAction for manual invite link sharing - #66

Merged
scottgrayson merged 6 commits into
2.xfrom
cursor/copy-invite-link-cb86
Oct 19, 2026
Merged

scottgrayson merged 6 commits into
2.xfrom
cursor/copy-invite-link-cb86

Conversation

@scottgrayson

@scottgrayson scottgrayson commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

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 on InviteAction.

  • Email invite stays the default; existing InviteAction / InviteBulkAction behavior is unchanged for the send path.
  • Copy-link has a different UX (modal with generate + copyable URL vs confirmation-to-send-email).
  • Opt-in: apps add the action only where needed.

Shared URL building lives in Tapp\FilamentInvite\Support\InviteUrl, used by both SetPassword mail and the copy-link path so the URL (route / getResetPasswordUrl() override, invite=1 extended expiry) stays in sync.

Explicit generation (avoids silent token invalidation)

Opening the modal does not mint a broker token. Pattern used:

  1. Modal opens with an empty form and a warning that generating invalidates earlier invite links (including emailed ones).
  2. Submit is labeled Generate link.
  3. On submit, createInviteLink() runs, the schema is filled with the URL, and $action->halt() keeps the modal open so the admin can copy.
  4. Generating again intentionally replaces the previous token (documented + tested).

This is the standard Filament 4/5 “submit → fill schema → halt()” keep-open pattern (same idea as create-another), rather than minting in fillForm on mount.

Eligibility

InviteAction and CopyInviteLinkAction both use hidden(fn (Model $user) => ! $this->canInviteUser($user)). canInviteUser() is the single source of truth (requires MustVerifyEmail, unverified, authenticated admin with update ability) and no longer null-derefs auth()->user().

Extended expiry for Filament panel reset URLs

Apps using the README getResetPasswordUrl() override (signed filament.{panel}.auth.password-reset.reset) were stuck with the default 60-minute broker expiry. Fixed by:

  1. InviteUrl::for() passes ['invite' => 1] into getResetPasswordUrl($token, $parameters) so the signed panel URL includes the invite flag.
  2. InviteMiddleware matches both password.reset and filament.*.auth.password-reset.reset. When the invite session is set, it extends the broker expire to filament-invite.expire on that request (including Livewire form submits via persistent middleware). Brokers are cleared after updating config so the new expire is used.
  3. Broker name comes from Filament::getAuthPasswordBroker() (falling back to auth.defaults.passwords), not a hard-coded users key.

Panel middleware wiring

Filament panels use their own middleware stack, not the web group. Auto-attach uses afterResolving(PanelRegistry::class) in packageRegistered() (not app.booted):

  • Runs after each PanelProvider has fully configured the panel (so InviteMiddleware is appended after StartSession).
  • Runs while the registry is still resolving, before Filament routes call 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())).
  • Still pushes onto the web group for non-panel password.reset routes.

The Filament 61-minute test uses an AdminPanelProvider registered like a real app (before boot) and asserts the boot-registered panel reset route already includes InviteMiddleware (and not web).

Usage

->recordActions([
    \Tapp\FilamentInvite\Actions\InviteAction::make(),
    \Tapp\FilamentInvite\Actions\CopyInviteLinkAction::make(),
])

Tests

  • URL-parity with SetPassword / getResetPasswordUrl().
  • Livewire action tests: visibility, no-mint-on-mount, generate, double-generate invalidation.
  • Expiry E2E: auto-attach / no duplicate plugin; Filament panel invite link (boot-registered route) → travel 61 minutes → Livewire reset succeeds; package password.reset invite; normal reset still expires at 60 minutes.

Local: Pest 19 passed, Pint clean, PHPStan level 4 clean. No version bump.

Open in Web Open in Cursor 

cursoragent and others added 6 commits October 19, 2026 17:15
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
scottgrayson marked this pull request as ready for review October 19, 2026 19:48
@scottgrayson
scottgrayson merged commit 974dbb2 into 2.x Oct 19, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants