From 11703c20e4d3ece2d2bd6335d9302a4ffdd350ad Mon Sep 17 00:00:00 2001 From: Manoj Hortulanus Date: Tue, 13 Jan 2026 15:47:02 +0100 Subject: [PATCH 1/7] Add user management --- .../7_add_deleted_add_to_users_tabe.php.stub | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 database/migrations/7_add_deleted_add_to_users_tabe.php.stub diff --git a/database/migrations/7_add_deleted_add_to_users_tabe.php.stub b/database/migrations/7_add_deleted_add_to_users_tabe.php.stub new file mode 100644 index 0000000..9d8318e --- /dev/null +++ b/database/migrations/7_add_deleted_add_to_users_tabe.php.stub @@ -0,0 +1,26 @@ +getTable(), 'deleted_at')) { + $table->softDeletes(); + } + }); + } + + public function down(): void + { + Schema::table(config('users.eloquent.user.table', 'users'), function (Blueprint $table) { + if(Schema::hasColumn($table->getTable(), 'deleted_at')) { + $table->dropSoftDeletes(); + } + }); + } +}; From 90987af28709da6ae2712ef4b46b047c87fcdc5e Mon Sep 17 00:00:00 2001 From: Mathieu Date: Fri, 30 Jan 2026 13:04:28 +0100 Subject: [PATCH 2/7] Remove matrix in github workflows --- .github/workflows/run-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index c878d51..e2b9b50 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -16,10 +16,10 @@ jobs: strategy: fail-fast: true matrix: - os: [ubuntu-latest, windows-latest] + os: [ubuntu-latest] php: [8.3, 8.2] laravel: [11.*, 10.*] - stability: [prefer-lowest, prefer-stable] + stability: [prefer-stable] include: - laravel: 11.* testbench: 9.* From 6e2093d2a8584439298c03ec556cdeb59b2d6e06 Mon Sep 17 00:00:00 2001 From: Bas van Dinther Date: Thu, 19 Feb 2026 11:00:23 +0100 Subject: [PATCH 3/7] perf: queue user login/logout recording to avoid slow DNS lookups (#83) The gethostbyaddr() call was blocking the login/logout response, causing delays of 1-30+ seconds depending on network conditions. Now the recording is dispatched to a queue job, so the user gets an instant response while the DNS lookup happens in the background. Co-authored-by: Claude Opus 4.5 --- src/Jobs/RecordUserLogin.php | 45 +++++++++++++++++++++++++ src/Listeners/Auth/HandleUserLogin.php | 26 +++++++------- src/Listeners/Auth/HandleUserLogout.php | 30 +++++++++-------- 3 files changed, 73 insertions(+), 28 deletions(-) create mode 100644 src/Jobs/RecordUserLogin.php diff --git a/src/Jobs/RecordUserLogin.php b/src/Jobs/RecordUserLogin.php new file mode 100644 index 0000000..5111aa0 --- /dev/null +++ b/src/Jobs/RecordUserLogin.php @@ -0,0 +1,45 @@ +|null $inputs + */ + public function __construct( + public int $userId, + public string $type, + public ?string $url, + public ?string $referrer, + public ?array $inputs, + public ?string $userAgent, + public ?string $ipAddress, + ) {} + + public function handle(): void + { + $userModel = config('users.eloquent.user.model'); + $user = $userModel::find($this->userId); + + if (! $user) { + return; + } + + $user->logins()->create([ + 'user_id' => $this->userId, + 'type' => $this->type, + 'url' => $this->url, + 'referrer' => $this->referrer, + 'inputs' => $this->inputs ? json_encode($this->inputs) : null, + 'user_agent' => $this->userAgent, + 'ip_address' => $this->ipAddress, + 'hostname' => $this->ipAddress ? gethostbyaddr($this->ipAddress) : null, + ]); + } +} diff --git a/src/Listeners/Auth/HandleUserLogin.php b/src/Listeners/Auth/HandleUserLogin.php index 3bbb348..aa0293e 100644 --- a/src/Listeners/Auth/HandleUserLogin.php +++ b/src/Listeners/Auth/HandleUserLogin.php @@ -2,28 +2,26 @@ namespace Backstage\Laravel\Users\Listeners\Auth; +use Backstage\Laravel\Users\Jobs\RecordUserLogin; use Illuminate\Auth\Events\Login; class HandleUserLogin { - public function handle(Login $event) + public function handle(Login $event): void { - /** - * @var \Backstage\Laravel\Users\Eloquent\Models\User $user - */ + /** @var \Backstage\Laravel\Users\Eloquent\Models\User $user */ $user = $event->user; $inputs = request()->except('_method', '_token', 'password'); - $user->logins()->create([ - 'user_id' => $user->id, - 'type' => 'login', - 'url' => request()->url(), - 'referrer' => request()->server('HTTP_REFERER'), - 'inputs' => count($inputs) ? json_encode($inputs) : null, - 'user_agent' => request()->server('HTTP_USER_AGENT'), - 'ip_address' => request()->ip(), - 'hostname' => gethostbyaddr(request()->ip()), - ]); + RecordUserLogin::dispatch( + userId: $user->id, + type: 'login', + url: request()->url(), + referrer: request()->server('HTTP_REFERER'), + inputs: count($inputs) ? $inputs : null, + userAgent: request()->server('HTTP_USER_AGENT'), + ipAddress: request()->ip(), + ); } } diff --git a/src/Listeners/Auth/HandleUserLogout.php b/src/Listeners/Auth/HandleUserLogout.php index 47f5144..7393a31 100644 --- a/src/Listeners/Auth/HandleUserLogout.php +++ b/src/Listeners/Auth/HandleUserLogout.php @@ -2,28 +2,30 @@ namespace Backstage\Laravel\Users\Listeners\Auth; +use Backstage\Laravel\Users\Jobs\RecordUserLogin; use Illuminate\Auth\Events\Logout; class HandleUserLogout { - public function handle(Logout $event) + public function handle(Logout $event): void { - /** - * @var \Backstage\Laravel\Users\Eloquent\Models\User $user - */ + /** @var \Backstage\Laravel\Users\Eloquent\Models\User|null $user */ $user = $event->user; + if (! $user) { + return; + } + $inputs = request()->except('_method', '_token', 'password'); - $user->logins()->create([ - 'user_id' => $user->id, - 'type' => 'logout', - 'url' => request()->url(), - 'referrer' => request()->server('HTTP_REFERER'), - 'inputs' => count($inputs) ? json_encode($inputs) : null, - 'user_agent' => request()->server('HTTP_USER_AGENT'), - 'ip_address' => request()->ip(), - 'hostname' => gethostbyaddr(request()->ip()), - ]); + RecordUserLogin::dispatch( + userId: $user->id, + type: 'logout', + url: request()->url(), + referrer: request()->server('HTTP_REFERER'), + inputs: count($inputs) ? $inputs : null, + userAgent: request()->server('HTTP_USER_AGENT'), + ipAddress: request()->ip(), + ); } } From 2380191fd3d11f133ad71c224128d72f63d2b869 Mon Sep 17 00:00:00 2001 From: Manoj Hortulanus Date: Thu, 19 Feb 2026 11:26:01 +0100 Subject: [PATCH 4/7] fix: guard against missing or invalid user model config in RecordUserLogin job Co-Authored-By: Claude Sonnet 4.6 --- src/Jobs/RecordUserLogin.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Jobs/RecordUserLogin.php b/src/Jobs/RecordUserLogin.php index 5111aa0..84618e5 100644 --- a/src/Jobs/RecordUserLogin.php +++ b/src/Jobs/RecordUserLogin.php @@ -25,6 +25,11 @@ public function __construct( public function handle(): void { $userModel = config('users.eloquent.user.model'); + + if (! $userModel || ! class_exists($userModel)) { + return; + } + $user = $userModel::find($this->userId); if (! $user) { From b816834a68d38ddf84870a0545aae2dbbcc7d793 Mon Sep 17 00:00:00 2001 From: Baspa Date: Fri, 13 Mar 2026 09:49:39 +0100 Subject: [PATCH 5/7] chore: upgrade Laravel Pint to 1.29.0 and apply code style fixes Upgraded Pint from 1.27.1 to 1.29.0 to align local development with CI workflow. Applied new code style rules across 225 files including: - fully_qualified_strict_types - ordered_imports - braces_position - class_definition Co-Authored-By: Claude Opus 4.5 --- config/users.php | 16 +++++++++++----- helpers.php | 7 +++++-- src/Eloquent/Observers/UserObserver.php | 3 ++- src/Events/Auth/UserCreated.php | 2 +- src/LaravelUsersServiceProvider.php | 24 ++++++++++++++++-------- src/Listeners/Auth/HandleUserLogin.php | 3 ++- src/Listeners/Auth/HandleUserLogout.php | 3 ++- 7 files changed, 39 insertions(+), 19 deletions(-) diff --git a/config/users.php b/config/users.php index 310bede..b0ce898 100644 --- a/config/users.php +++ b/config/users.php @@ -1,5 +1,11 @@ [ 'user' => [ - 'model' => \Backstage\Laravel\Users\Eloquent\Models\User::class, + 'model' => User::class, 'table' => 'users', - 'observer' => \Backstage\Laravel\Users\Eloquent\Observers\UserObserver::class, + 'observer' => UserObserver::class, ], 'user_login' => [ - 'model' => \Backstage\Laravel\Users\Eloquent\Models\UserLogin::class, + 'model' => UserLogin::class, 'table' => 'user_logins', ], 'user_notification_preferences' => [ - 'model' => \Backstage\Laravel\Users\Eloquent\Models\UserNotificationPreference::class, + 'model' => UserNotificationPreference::class, 'table' => 'user_notification_preferences', ], ], @@ -27,7 +33,7 @@ 'auth' => [ 'user_created' => [ // Or set Backstage\Filament\Users\Notifications\UserInvitationNotification - 'invitation_notification' => \Backstage\Laravel\Users\Notifications\Invitation::class, + 'invitation_notification' => Invitation::class, 'notification_delivery_channels' => [ 'mail', ], diff --git a/helpers.php b/helpers.php index 5dc6c72..f168c61 100644 --- a/helpers.php +++ b/helpers.php @@ -1,5 +1,8 @@ app->make(\Illuminate\Contracts\Http\Kernel::class); + $kernel = $this->app->make(Kernel::class); }); - if (config('users.eloquent.user.observer', \Backstage\Laravel\Users\Eloquent\Observers\UserObserver::class)) { - config('auth.providers.users.model', \Backstage\Laravel\Users\Eloquent\Models\User::class)::observe(config('users.eloquent.user.observer', \Backstage\Laravel\Users\Eloquent\Observers\UserObserver::class)); + if (config('users.eloquent.user.observer', UserObserver::class)) { + config('auth.providers.users.model', User::class)::observe(config('users.eloquent.user.observer', UserObserver::class)); } } protected function getEvents() { $this->app['events']->listen( - \Illuminate\Auth\Events\Login::class, - \Backstage\Laravel\Users\Listeners\Auth\HandleUserLogin::class + Login::class, + HandleUserLogin::class ); $this->app['events']->listen( - \Illuminate\Auth\Events\Logout::class, - \Backstage\Laravel\Users\Listeners\Auth\HandleUserLogout::class + Logout::class, + HandleUserLogout::class ); if (config('users.events.auth.user_created.enabled', true)) { $this->app['events']->listen( UserCreated::class, - \Backstage\Laravel\Users\Listeners\Auth\SendInvitationMail::class + SendInvitationMail::class ); } } diff --git a/src/Listeners/Auth/HandleUserLogin.php b/src/Listeners/Auth/HandleUserLogin.php index aa0293e..b7e3a35 100644 --- a/src/Listeners/Auth/HandleUserLogin.php +++ b/src/Listeners/Auth/HandleUserLogin.php @@ -2,6 +2,7 @@ namespace Backstage\Laravel\Users\Listeners\Auth; +use Backstage\Laravel\Users\Eloquent\Models\User; use Backstage\Laravel\Users\Jobs\RecordUserLogin; use Illuminate\Auth\Events\Login; @@ -9,7 +10,7 @@ class HandleUserLogin { public function handle(Login $event): void { - /** @var \Backstage\Laravel\Users\Eloquent\Models\User $user */ + /** @var User $user */ $user = $event->user; $inputs = request()->except('_method', '_token', 'password'); diff --git a/src/Listeners/Auth/HandleUserLogout.php b/src/Listeners/Auth/HandleUserLogout.php index 7393a31..5a565ff 100644 --- a/src/Listeners/Auth/HandleUserLogout.php +++ b/src/Listeners/Auth/HandleUserLogout.php @@ -2,6 +2,7 @@ namespace Backstage\Laravel\Users\Listeners\Auth; +use Backstage\Laravel\Users\Eloquent\Models\User; use Backstage\Laravel\Users\Jobs\RecordUserLogin; use Illuminate\Auth\Events\Logout; @@ -9,7 +10,7 @@ class HandleUserLogout { public function handle(Logout $event): void { - /** @var \Backstage\Laravel\Users\Eloquent\Models\User|null $user */ + /** @var User|null $user */ $user = $event->user; if (! $user) { From d091ce45010b5985f1b89cc8ea87a5fdf39248cc Mon Sep 17 00:00:00 2001 From: Mark van Eijk Date: Wed, 25 Mar 2026 10:09:59 +0100 Subject: [PATCH 6/7] Remove spatie/laravel-ray dependency from all packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dependency is unused — no ray() calls exist in the codebase. Removes it from require-dev in 9 packages, the conflict section in laravel-mails, the CI workflow removal step, and the laravel-ai configure script. Co-Authored-By: Claude Opus 4.6 (1M context) --- composer.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 20f1e56..d47764d 100644 --- a/composer.json +++ b/composer.json @@ -32,8 +32,7 @@ "pestphp/pest-plugin-laravel": "^2.3", "phpstan/extension-installer": "^1.3", "phpstan/phpstan-deprecation-rules": "^1.1", - "phpstan/phpstan-phpunit": "^1.3", - "spatie/laravel-ray": "^1.35" + "phpstan/phpstan-phpunit": "^1.3" }, "autoload": { "psr-4": { From d183b03e5ac26904f1b20d6ec6d1c91fd10a7ac3 Mon Sep 17 00:00:00 2001 From: markvaneijk <1925388+markvaneijk@users.noreply.github.com> Date: Tue, 31 Mar 2026 03:35:24 +0000 Subject: [PATCH 7/7] Fix styling --- helpers.php | 2 +- src/Console/Commands/DeleteUser.php | 6 +++--- src/Console/Commands/ListUsersCommand.php | 6 +++--- src/Console/Commands/MakeUserCommand.php | 2 +- src/Domain/Email/Actions/ValidateEmail.php | 2 +- src/LaravelUsersServiceProvider.php | 2 +- tests/TestCase.php | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/helpers.php b/helpers.php index f168c61..58db9b3 100644 --- a/helpers.php +++ b/helpers.php @@ -7,7 +7,7 @@ function geo($attribute = '') { if (! session('geo')) { - $geo = json_decode(@file_get_contents('https://pro.ip-api.com/json/' . request()->ip() . '?key=' . config('services.ip-api.key'))); + $geo = json_decode(@file_get_contents('https://pro.ip-api.com/json/'.request()->ip().'?key='.config('services.ip-api.key'))); session()->put('geo', $geo); } else { diff --git a/src/Console/Commands/DeleteUser.php b/src/Console/Commands/DeleteUser.php index 4272297..881fffb 100644 --- a/src/Console/Commands/DeleteUser.php +++ b/src/Console/Commands/DeleteUser.php @@ -18,7 +18,7 @@ class DeleteUser extends Command public function handle() { if ($this->option('force-delete') && posix_geteuid() !== 0) { - error('This command must be run as root. Try: sudo php artisan ' . str($this->signature)->replace(['{', '}'], '')->toString()); + error('This command must be run as root. Try: sudo php artisan '.str($this->signature)->replace(['{', '}'], '')->toString()); return Command::FAILURE; } @@ -33,7 +33,7 @@ public function handle() $users = multiselect( label: 'Select the user(s) to delete', - options: $userCollection->pluck('name', 'id')->map(fn ($name, $id) => $name . ' (ID: ' . $id . ')')->toArray(), + options: $userCollection->pluck('name', 'id')->map(fn ($name, $id) => $name.' (ID: '.$id.')')->toArray(), required: true, ); @@ -59,7 +59,7 @@ public function handle() $user->delete(); } - $this->info($this->option('force-delete') ? 'Force deleted' : 'Deleted' . ' user: ' . $user->name); + $this->info($this->option('force-delete') ? 'Force deleted' : 'Deleted'.' user: '.$user->name); } } } diff --git a/src/Console/Commands/ListUsersCommand.php b/src/Console/Commands/ListUsersCommand.php index 63b0449..6bc41b0 100644 --- a/src/Console/Commands/ListUsersCommand.php +++ b/src/Console/Commands/ListUsersCommand.php @@ -31,7 +31,7 @@ public function handle(): void return; } - info('Found ' . $users->count() . ' user(s):'); + info('Found '.$users->count().' user(s):'); $this->renderTable($users); @@ -46,7 +46,7 @@ public function handle(): void $userId = search( label: 'Search for the user that should receive the mail', options: fn (string $value) => strlen($value) > 0 - ? User::whereLike('name', "%{$value}%")->pluck('name', 'id')->map(fn ($name, $id) => $name . ' (ID: ' . $id . ')')->toArray() + ? User::whereLike('name', "%{$value}%")->pluck('name', 'id')->map(fn ($name, $id) => $name.' (ID: '.$id.')')->toArray() : [] ); @@ -169,7 +169,7 @@ protected function renderTable(Collection $users) $user->email, $user->hasVerifiedEmail() ? 'Yes' : 'No', $user->getRoleNames()->implode(', ') ?: 'No roles', - $user->created_at->format('Y-m-d H:i:s') . ' (' . $user->created_at->diffForHumans() . ')', + $user->created_at->format('Y-m-d H:i:s').' ('.$user->created_at->diffForHumans().')', ])->toArray()); } diff --git a/src/Console/Commands/MakeUserCommand.php b/src/Console/Commands/MakeUserCommand.php index 2809329..7450349 100644 --- a/src/Console/Commands/MakeUserCommand.php +++ b/src/Console/Commands/MakeUserCommand.php @@ -106,7 +106,7 @@ public function handle(): int $this->line("Name: {$user->name}"); $this->line("Email: {$user->email}"); if ($selectedRole) { - $this->line('Role: ' . ($selectedRole instanceof Role ? $selectedRole->name : $selectedRole)); + $this->line('Role: '.($selectedRole instanceof Role ? $selectedRole->name : $selectedRole)); } $this->line("Password: {$password}"); diff --git a/src/Domain/Email/Actions/ValidateEmail.php b/src/Domain/Email/Actions/ValidateEmail.php index c8e0f25..31dff41 100644 --- a/src/Domain/Email/Actions/ValidateEmail.php +++ b/src/Domain/Email/Actions/ValidateEmail.php @@ -8,7 +8,7 @@ class ValidateEmail { use AsAction; - public function handle(string | array $email): bool | array + public function handle(string|array $email): bool|array { if (is_array($email)) { return $this->validateMultipleEmails($email); diff --git a/src/LaravelUsersServiceProvider.php b/src/LaravelUsersServiceProvider.php index 1aadd93..c373859 100644 --- a/src/LaravelUsersServiceProvider.php +++ b/src/LaravelUsersServiceProvider.php @@ -39,7 +39,7 @@ public function configurePackage(Package $package): void protected function getMigrations(): array { - $migrationPath = __DIR__ . '/../database/migrations/'; + $migrationPath = __DIR__.'/../database/migrations/'; $files = File::allFiles($migrationPath); diff --git a/tests/TestCase.php b/tests/TestCase.php index 6d7fda3..35852ea 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -13,7 +13,7 @@ protected function setUp(): void parent::setUp(); Factory::guessFactoryNamesUsing( - fn (string $modelName) => 'Backstage\\Laravel\\Users\\Database\\Factories\\' . class_basename($modelName) . 'Factory' + fn (string $modelName) => 'Backstage\\Laravel\\Users\\Database\\Factories\\'.class_basename($modelName).'Factory' ); }