PHPackages                             pijler/user-devices - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. pijler/user-devices

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

pijler/user-devices
===================

This package provides support for managing user devices.

v1.1.0(1mo ago)0302↓50%1MITPHPPHP ^8.3CI passing

Since Jan 31Pushed 1mo agoCompare

[ Source](https://github.com/Pijler/user-devices)[ Packagist](https://packagist.org/packages/pijler/user-devices)[ RSS](/packages/pijler-user-devices/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (7)Dependencies (10)Versions (11)Used By (1)

📱 Laravel User Devices
======================

[](#-laravel-user-devices)

This package provides support for managing user devices in Laravel. Track login devices, detect new logins, send notifications, and allow users to block suspicious devices—useful for security monitoring, session management, and multi-device applications.

### 🧩 Features

[](#-features)

- ✅ **Device tracking**: Automatic tracking of IP address, user agent, location, and session ID
- ✅ **New login detection**: Identifies first-time logins from new devices
- ✅ **Email notifications**: Sends alerts when a new device logs in, on login attempts, and on failed logins
- ✅ **Configurable events**: Enable or disable listeners per auth event (authenticated, attempting, failed)
- ✅ **Location from IP**: Optional geolocation via callback
- ✅ **Block device**: Signed links to block suspicious devices in all notification types (invalidates session when blocked)
- ✅ **Integrated middleware**: Protect routes from blocked devices
- ✅ **Block login check**: Prevent blocked devices from attempting login via `isCurrentDeviceBlocked()`
- ✅ **Model trait**: Simple Eloquent integration
- ✅ **Flexible configuration**: Custom models and callbacks

### 📦 Installation

[](#-installation)

You can install the package via Composer:

```
composer require pijler/user-devices
```

### 🗄️ Publishing

[](#️-publishing)

Publish the package config (optional):

```
php artisan vendor:publish --tag=user-devices-config
```

Publish the package migrations:

```
php artisan vendor:publish --tag=user-devices-migrations
```

Run the migrations:

```
php artisan migrate
```

### ⚙️ Configuration

[](#️-configuration)

#### Config File

[](#config-file)

```
// config/user-devices.php
return [
    'events' => [
        'failed' => true,          // Track failures, notify when new device
        'attempting' => false,     // Track attempts, notify when new device
        'authenticated' => true,   // Save device + send new login notification
    ],
    'credential_key' => 'email',   // Key to find user from credentials (attempting/failed)
];
```

#### DeviceCreator

[](#devicecreator)

The package works out-of-the-box, but you can customize the behavior:

```
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\URL;
use UserDevices\DeviceCreator;
use UserDevices\Notifications\AuthenticatedLoginNotification;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Use custom user model
        DeviceCreator::useUserModel(CustomUser::class);

        // Use custom user device model
        DeviceCreator::useUserDeviceModel(CustomUserDevice::class);

        // Customize user agent generation
        DeviceCreator::userAgentUsing(fn ($userAgent) => substr($userAgent, 0, 255));

        // Control when to send notifications (e.g. disable in local/staging)
        DeviceCreator::shouldSendNotificationUsing(function ($user, $device) {
            return ! app()->environment('local');
        });

        // Resolve location from IP (optional)
        DeviceCreator::resolveLocationUsing(function (string $ip) {
            $geo = geoip($ip);
            return $geo->city ? "{$geo->city}, {$geo->country}" : $geo->country;
        });

        // Customize the notification email
        AuthenticatedLoginNotification::toMailUsing(function ($notifiable, $device) {
            $expire = Config::get('auth.verification.expire', 60);

            $blockUrl = URL::temporarySignedRoute(
                name: 'user-devices.block',
                expiration: Carbon::now()->addMinutes($expire),
                parameters: [
                    'id' => $device->getKey(),
                    'hash' => sha1($device->getKey()),
                ],
            );

            return (new MailMessage)
                ->subject('New device detected')
                ->line('We detected a new login to your account.')
                ->action('Block device', $blockUrl);
        });

        // Customize the block device URL
        AuthenticatedLoginNotification::createBlockUrlUsing(function ($device) {
            return URL::temporarySignedRoute(
                name: 'your-custom-route-name',
                expiration: Carbon::now()->addMinutes(120),
                parameters: [
                    'id' => $device->getKey(),
                    'hash' => sha1($device->getKey()),
                ],
            );
        });
    }
}
```

The block link expiration uses `auth.verification.expire` (default 60 minutes), same as Laravel's email verification.

### 🧠 Usage

[](#-usage)

#### 1. Using the HasUserDevices Trait

[](#1-using-the-hasuserdevices-trait)

Add the `HasUserDevices` trait to your User model. Your User model must also use the `Notifiable` trait for notifications:

```
use Illuminate\Notifications\Notifiable;
use UserDevices\Traits\HasUserDevices;

class User extends Authenticatable
{
    use HasUserDevices;
    use Notifiable;
}
```

#### 2. Saving User Devices

[](#2-saving-user-devices)

The package automatically saves user devices when auth events fire. No manual setup required—just add the `HasUserDevices` trait to your User model.

- **Authenticated**: Saves/updates device (IP, user agent, location, session ID). Sends notification only on first login from that device.
- **Attempting**: Same as above. Finds user by email in credentials (when `events.attempting` is enabled).
- **Failed**: Same as above. Uses user from event or resolves from credentials (when `events.failed` is enabled).

All three events use `firstOrNew` by IP + user agent, so the same device is updated across requests.

To skip saving the device entirely for a request (e.g. in middleware or controller before authentication):

```
use UserDevices\DeviceCreator;

DeviceCreator::ignoreListener();
```

To ignore only the new login notification (device is still saved, but no email is sent):

```
DeviceCreator::ignoreNotification();
```

To control notifications globally (e.g. disable in local/staging, or custom logic per user/device):

```
DeviceCreator::shouldSendNotificationUsing(fn () => false);
DeviceCreator::shouldSendNotificationUsing(fn ($user, $device) => ! $user->isAdmin());
DeviceCreator::shouldSendNotificationUsing(fn ($user, $device) => app()->environment('production'));
```

#### 3. Block Device Route

[](#3-block-device-route)

When a user receives the new login notification email, they can click a link to block the device. Register a route that handles this request. Blocking invalidates the device's session when using session-based auth. The route must be **signed** and named `user-devices.block`:

```
use UserDevices\Http\Requests\BlockDeviceRequest;

Route::get('/devices/block/{id}/{hash}', function (BlockDeviceRequest $request) {
    $request->fulfill();

    return redirect()->route('home')->with('message', 'Device blocked successfully.');
})->middleware(['signed', 'throttle:6,1'])->name('user-devices.block');
```

You can use any path you prefer as long as the route is named `user-devices.block` and includes the `{id}` and `{hash}` parameters. All three notification types (Authenticated, Attempting, Failed) include a block link in the email.

#### 4. Check Blocked Device Before Login

[](#4-check-blocked-device-before-login)

To prevent blocked devices from attempting login, call `isCurrentDeviceBlocked()` after resolving the user (e.g. by email) and before validating the password. In a custom login controller or FormRequest:

```
// In your login logic, after resolving the user from credentials (e.g. email)
$user = User::where('email', $request->email)->first();

if ($user && $user->isCurrentDeviceBlocked()) {
    return response()->json(['message' => 'This device has been blocked.'], 423);
}

// Proceed with login attempt...
```

Or in a FormRequest's `authorize` or custom validation:

```
public function authorize(): bool
{
    $user = User::where('email', $this->email)->first();

    return ! ($user && $user->isCurrentDeviceBlocked());
}
```

#### 5. Using the Middleware

[](#5-using-the-middleware)

The package includes middleware to block requests from devices the user has blocked:

```
Route::middleware(['auth', 'check.device'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
});
```

When a blocked device tries to access a protected route, the middleware returns `423 Locked`.

#### 6. Working with the UserDevice Model

[](#6-working-with-the-userdevice-model)

```
use UserDevices\Models\UserDevice;

// Get user's devices
$devices = $user->userDevices;

// Block a device (invalidates session if session_id is set)
$device = UserDevice::find($id);
$device->block();

// Unblock a device
$device->unblock();

// Block by ID (static)
UserDevice::markAsBlocked($id);

// Unblock by ID (static)
UserDevice::markAsUnblocked($id);
```

#### 7. Sending Notifications Manually

[](#7-sending-notifications-manually)

```
$user->sendFailedLoginNotification($device);
$user->sendAttemptingLoginNotification($device);
$user->sendAuthenticatedLoginNotification($device);
```

#### 8. Customizing Attempting &amp; Failed Login Notifications

[](#8-customizing-attempting--failed-login-notifications)

```
use UserDevices\Notifications\AttemptingLoginNotification;
use UserDevices\Notifications\FailedLoginNotification;

AttemptingLoginNotification::toMailUsing(fn ($notifiable, $device) => (new MailMessage)
    ->subject('Login attempt')->line("IP: {$device->ip_address}"));

AttemptingLoginNotification::createBlockUrlUsing(fn ($device) => URL::temporarySignedRoute(/* ... */));

FailedLoginNotification::toMailUsing(fn ($notifiable, $device) => (new MailMessage)
    ->subject('Failed login')->line("IP: {$device->ip_address}"));

FailedLoginNotification::createBlockUrlUsing(fn ($device) => URL::temporarySignedRoute(/* ... */));
```

### 🧩 API Reference

[](#-api-reference)

#### DeviceCreator

[](#devicecreator-1)

```
// Configuration
DeviceCreator::useUserModel(string $model): void
DeviceCreator::useUserDeviceModel(string $model): void
DeviceCreator::userAgentUsing(Closure $callback): void
DeviceCreator::resolveLocationUsing(Closure $callback): void  // (string $ip) => ?string
DeviceCreator::shouldSendNotificationUsing(Closure $callback): void  // (user, device) => bool

// Request context (call before authentication)
DeviceCreator::ignoreListener(): void     // Skip saving the device for the current request
DeviceCreator::ignoreNotification(): void // Skip the new login notification for the current request
```

#### UserDevice Model

[](#userdevice-model)

```
// Relationships
$device->user(): BelongsTo

// Actions
$device->block(): void   // Also invalidates session when session_id is set
$device->unblock(): void

// Static methods
UserDevice::markAsBlocked(mixed $id): void
UserDevice::markAsUnblocked(mixed $id): void
```

#### HasUserDevices Trait

[](#hasuserdevices-trait)

```
// Methods available on model
$model->userDevices(): HasMany
$model->isCurrentDeviceBlocked(): bool  // Check if current request's device is blocked (use before login)
$model->sendFailedLoginNotification(UserDevice $device): void
$model->sendAttemptingLoginNotification(UserDevice $device): void
$model->sendAuthenticatedLoginNotification(UserDevice $device): void
```

#### AuthenticatedLoginNotification

[](#authenticatedloginnotification)

```
AuthenticatedLoginNotification::toMailUsing(Closure $callback): void
AuthenticatedLoginNotification::createBlockUrlUsing(Closure $callback): void
```

#### AttemptingLoginNotification &amp; FailedLoginNotification

[](#attemptingloginnotification--failedloginnotification)

```
AttemptingLoginNotification::toMailUsing(Closure $callback): void
AttemptingLoginNotification::createBlockUrlUsing(Closure $callback): void

FailedLoginNotification::toMailUsing(Closure $callback): void
FailedLoginNotification::createBlockUrlUsing(Closure $callback): void
```

#### BlockDeviceRequest

[](#blockdevicerequest)

```
$request->fulfill(): void
$request->getDevice(): ?UserDevice
```

### 📝 License

[](#-license)

Open-source under the [MIT license](LICENSE).

🚀 Thanks!
---------

[](#-thanks)

###  Health Score

45

—

FairBetter than 93% of packages

Maintenance89

Actively maintained with recent releases

Popularity17

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity55

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~8 days

Total

7

Last Release

55d ago

Major Versions

v0.3.0 → v1.0.02026-02-10

PHP version history (2 changes)v0.1.0PHP ^8.2

v1.1.0PHP ^8.3

### Community

Maintainers

![](https://www.gravatar.com/avatar/5256615ef2ba165b92bdf89908082480fb12be597c607637262e9fe61b5b2b22?d=identicon)[joaopalopes24@gmail.com](/maintainers/joaopalopes24@gmail.com)

---

Top Contributors

[![joaopalopes24](https://avatars.githubusercontent.com/u/45684782?v=4)](https://github.com/joaopalopes24 "joaopalopes24 (34 commits)")

---

Tags

laravelpackageDevicesuser-devices

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/pijler-user-devices/health.svg)

```
[![Health](https://phpackages.com/badges/pijler-user-devices/health.svg)](https://phpackages.com/packages/pijler-user-devices)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
