Skip to content

Admin User Invites

The Filament admin panel does not let users sign up. Every user is created by an existing admin, who triggers a welcome email containing a password-setup link. This page covers the invite flow, the token-expiry settings, and the signals admins use to know whether an invited user has actually accessed the panel.

When an admin creates a user from /admin/users:

  1. A random password is set on the user record (the user never sees or uses it).
  2. A Laravel password-reset token is generated via Password::broker()->createToken().
  3. A WelcomeUserNotification email is queued, containing the Filament reset URL: Filament::getPanel('admin')->getResetPasswordUrl($token, $user).
  4. The user clicks the link, sets a password, and is auto-logged into the panel on submit (Filament’s default behaviour for the reset page).

All three steps are encapsulated in App\Services\User\UserInviteService::send(), which is called from both the create flow and the resend action.

Reset tokens (including the welcome-email link) expire after 5 days, configured at passwords.users.expire = 7200 (minutes) in config/auth.php. The same setting applies to “Forgot password” links on /admin/login.

Expired tokens stay in the password_reset_tokens table until they are replaced (by a new invite or reset request). Laravel returns the same generic “Invalid token” error for both expired and structurally-invalid tokens.

If an invite link expires or is lost, admins can resend it from /admin/users/{id}/edit using the Resend invite header action. The action:

  • Generates a fresh token (replacing any previous row in password_reset_tokens).
  • Builds a new Filament reset URL.
  • Re-sends WelcomeUserNotification.

The old invite link stops working the moment Resend is clicked, because its token hash no longer matches the new row. The user should always use the most recent email they received.

Resend is restricted to users with the admin role and is hidden on the admin’s own user record.

Knowing whether an invited user has accessed the panel

Section titled “Knowing whether an invited user has accessed the panel”

Every login bumps users.last_login_at. This is the authoritative signal for “has this user actually accessed the app?”:

  • last_login_at IS NULL — the user has never logged in. If the invite was sent more than a few minutes ago, the link is probably unused and a resend may be needed.
  • last_login_at IS NOT NULL — the user has accessed the panel at least once. The displayed value is the most recent login moment and never expires.

The Users list (/admin/users) shows this as a sortable Last login column: a red “Never logged in” badge when null, a green relative date otherwise.

App\Listeners\UpdateLastLoginAt listens to Laravel’s Illuminate\Auth\Events\Login event and calls forceFill(['last_login_at' => now()])->saveQuietly() on the authenticated user. Auto-discovery picks it up — no manual registration in AppServiceProvider is needed.

When an admin starts impersonating a supplier via the Impersonate action, Laravel’s Auth::login($target) also fires a Login event for the target. To keep last_login_at an accurate signal of real user access, UserImpersonation::start() sets the static flag UserImpersonation::$loginIsImpersonation = true around the Auth::login() call. The listener checks this flag and skips the bump while it is set.

The flag is cleared in a finally block so a failure inside Auth::login() cannot leave it stuck.

What the password_reset_tokens table does not tell you

Section titled “What the password_reset_tokens table does not tell you”

It is tempting to use “has a row in password_reset_tokens” as a proxy for “invite not yet used”. This is unreliable in practice, because the same table is also used by any user who clicks Forgot password on /admin/login. A long-term user who once clicked Forgot Password without finishing will show up as “pending” even though they have a working password and use the panel daily. Always rely on last_login_at instead.

  • app/Services/User/UserInviteService.php — shared invite logic.
  • app/Filament/Resources/Users/Pages/CreateUser.php — first-time invite.
  • app/Filament/Resources/Users/Pages/EditUser.php — hosts the Resend action in the page header.
  • app/Filament/Resources/Users/Actions/ResendInviteAction.php — the action itself, with the admin-only visibility check.
  • app/Notifications/WelcomeUserNotification.php — the email body.
  • app/Listeners/UpdateLastLoginAt.php — the Login-event listener.
  • app/Services/UserImpersonation.php — sets the $loginIsImpersonation flag during Auth::login($target).
  • config/auth.phppasswords.users.expire (minutes).