kreetancraft/laravel-user-management

Complete user management for Laravel — Livewire 4 + Flux UI, Fortify 2FA/passkeys, roles & permissions, impersonation, login history.
41
Install
composer require kreetancraft/laravel-user-management
Latest Version:0.7.0
PHP:^8.2
License:MIT
Last Updated:Aug 29, 2026
Links: GitHub  ·  Packagist
Maintainer: kritaneducation

kreetancraft/laravel-user-management

Users, roles and permissions for Laravel — Livewire 4 + Flux UI, Fortify (2FA, passkeys, email verification, password reset), spatie/laravel-permission, impersonation, and login history.

Standalone package. No nwidart/laravel-modules, no bundled CSS, no bundled layouts.

Design decisions worth knowing before you install

It ships no CSS and no layouts. The admin screens render into your layout and inherit your Tailwind + Flux theme. Buttons use variant="primary", so they follow your Flux accent colour automatically. You must provide the two layouts named in config('user-management.layouts').

It seeds nothing but the super admin. Permissions are generated from your policies by user-management:sync-permissions; roles are created at runtime through the UI by the super admin. This follows Filament Shield's model. Nothing in the package assumes anything about your application's domain.

It logs nothing. There is no audit trail here. Instead it emits domain events — UserCreated, UserInvited, UserUpdated, UserDeleted, UserDeactivated, RoleCreated — for an audit package to subscribe to.

It handles no images. There are no avatars and no media library. User::avatarUrl() returns null and is the extension point: override it on a subclass and the views pick it up.

Flux is a hard dependency. Every admin screen uses <flux:*>. The package uses only free-tier Flux components, but livewire/flux must be installed.

Requirements

  • PHP ^8.2, Laravel ^12|^13
  • livewire/livewire ^4, livewire/flux ^2, laravel/fortify ^1.37
  • spatie/laravel-permission ^8, spatie/laravel-data ^4, spatie/laravel-query-builder ^7
  • lorisleiva/laravel-actions, sandermuller/laravel-fluent-validation

Optional, enabled per feature flag: lab404/laravel-impersonate, torann/geoip.

Installation

composer require kreetancraft/laravel-user-management
php artisan vendor:publish --tag=user-management-config
php artisan migrate
php artisan user-management:super-admin
php artisan user-management:sync-permissions

Point auth at the package's User model

In config/auth.php:

'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => Kreetancraft\UserManagement\Models\User::class,
    ],
],

Or extend it, which is how you add avatars or your own relations:

class User extends \Kreetancraft\UserManagement\Models\User {}

Extending is the recommendation, not a requirement. Keeping the App\Models\User a fresh Laravel install gives you — extending Illuminate\Foundation\Auth\User, with Spatie's HasRoles — works: the package asks the person performing an action only what any authenticatable can answer. What you give up is the model's own conveniences (avatarUrl(), isSuperAdmin(), the login-history relation) on your own class.

Let Tailwind see this package

Required. Tailwind v4 generates only the classes it finds by scanning files, and it does not scan vendor/. In resources/css/app.css:

@source '../../vendor/kreetancraft/laravel-user-management/resources/views';

Skipping it fails confusingly rather than loudly — classes shared with your own views still work and only the ones unique to this package go missing.

Provide the layouts

Defaults match a stock Laravel starter kit. Note the asymmetry — it is Laravel's, not ours: the admin screens are Livewire pages and ->layout() takes a view name, while the Fortify auth screens are plain Blade wrapped in <x-dynamic-component>, which takes a component name.

'layouts' => [
    'admin' => 'components.layouts.app', // view name
    'auth'  => 'layouts.auth',           // component name
],

The sidebar

Include the nav once, anywhere in your layout:

<flux:navlist.group heading="Admin">
    <x-user-management::nav />
</flux:navlist.group>

That renders every admin link — this package's, and any other package's. There is no list to maintain: user-management:install will inject the line for you, or add it by hand.

Links carrying a group render under a heading. This package puts its own two under Users; rename or remove that heading with user-management.navigation.group.

Do not publish the nav. It has its own tag (user-management-nav) and is excluded from user-management-views on purpose. A published copy wins over the package's, so one taken before a feature existed keeps rendering the old way after an upgrade — which is how sidebar grouping silently went missing for anyone who published views at 0.7.0. Publish the screens freely; publish the nav only if you mean to own it forever.

If your sidebar is not grouping, that copy is why:

php artisan vendor:publish --tag=user-management-nav --force

Adding your own links

app(\Kreetancraft\UserManagement\Navigation::class)->add([
    'label' => 'Reports',
    'icon' => 'chart-bar',
    'route' => 'admin.reports',
    'ability' => 'view-reports',   // optional
    'sort' => 40,
]);

Adding links from another package

A package cannot depend on this one just to appear in a sidebar, so the seam is a container tag — bind an item, tag it admin.navigation, done:

// in any service provider. No mention of this package anywhere.
$this->app->bind('acme.navigation.items', fn () => [[
    'label' => __('Invoices'),
    'icon' => 'document-text',
    'route' => 'admin.invoices',
    'ability' => 'viewAny',
    'model' => Invoice::class,
    'sort' => 30,
]]);

$this->app->tag('acme.navigation.items', 'admin.navigation');

Why a tag rather than a facade call: tags are collected at render time, so provider order does not matter, and a binding nobody collects is never resolved. The contributing package keeps working unchanged when this one is not installed.

kreetancraft/laravel-media-manager does exactly this — install it and a Media link appears, with nothing declared on either side.

Key
label Required. Already translated — pass __('…') yourself.
route Required, a route name. Skipped silently if the route does not exist, so a package whose routes are switched off cannot break the sidebar.
ability Optional. With model, the ordinary policy question; without, a bare ability check. Omit to always show.
model Optional. Pass it when a policy decides, so the link appears exactly when the page behind it is reachable.
icon Optional, a Flux/Heroicon name. Defaults to square-2-stack.
sort Optional, defaults to 50. This package uses 10 (Users) and 20 (Roles).

Avatars

This package ships no image handling. Point it at one that does and an avatar field appears on the user forms:

// config/user-management.php
'avatar_resolver'   => \Kreetancraft\Media\Support\MediaAvatarResolver::class,
'media_picker_view' => 'media::picker-field',

Both halves are needed — one to store the avatar, one to choose it. With either missing the field renders nothing and the forms are exactly as they were, rather than showing a control with nothing behind it. If you expected a field and there is none, php artisan user-management:avatar-doctor names the reason.

On a page with no form of its own — a profile page, say — use the Livewire component. It listens for the pick and saves for itself:

<livewire:user-management.avatar />                        {{-- the signed-in user --}}
<livewire:user-management.avatar :user="$someone" />       {{-- someone else; needs update-users --}}

Dropping the Blade field there instead would render a picker that quietly did nothing: the field relies on a surrounding component to hear media-picked, which the user forms provide and a profile page does not.

On a profile page, prefer the uploader. The chooser opens the whole media library and needs a permission over it; someone setting their own picture needs neither:

// config/user-management.php
'avatar_uploader' => 'media.avatar-uploader',

The profile component then uploads, while the admin user forms keep the chooser.

The package ships no profile screen on purpose — yours is already yours.

Inside a Livewire form of your own, use the Blade field and bind it, so an unsaved choice survives until submit:

<x-user-management::avatar-field :items="$avatarMedia" />

When something does not appear

An avatar field that is missing, a sidebar that will not group, a picker that opens the library when you asked for an uploader — these all fail the same way: nothing renders, and there is no error. That is correct behaviour on an install without the optional pieces, and indistinguishable from a misconfiguration. Ask:

php artisan user-management:avatar-doctor

It prints the installed version of this package and of laravel-media-manager, then checks the resolver, whether its class is installed, whether that resolver can write and not only read, the picker view, whether an uploader is configured and registered, and whether a published copy of the user forms or the profile picker is shadowing the package's.

Two things account for most of it. These packages wire together through config values rather than Composer constraints — deliberate, so neither requires the other — and nothing stops one being older than the other. And a published view wins over the package's, so a copy taken before a feature existed keeps rendering the old way after an upgrade. Both are silent; the command makes them visible.

Configuration

config/user-management.php:

'super_admin' => ['role' => 'super-admin', 'enabled' => true],

'features' => [
    'two_factor' => true, 'passkeys' => true,
    'impersonation' => true, 'login_history' => true,
    'registration' => false,
],

// Colours. Roles are runtime rows, so a colour is chosen by hashing the role
// name against this palette — stable everywhere, nothing to configure per role.
'ui' => [
    'role_palette' => ['blue', 'emerald', 'violet', 'amber', 'cyan', 'pink', 'lime', 'indigo', 'teal', 'orange'],
    'super_admin_color' => 'red',
    'status' => ['active' => 'emerald', 'inactive' => 'zinc'],
],

'routes' => [
    'prefix' => 'admin',
    'middleware' => ['web', 'auth', 'verified', 'ensure.2fa.enforced'],
    'names' => [
        // Required only if you enforce 2FA per user: the route where they enable it.
        'security_edit' => null,
    ],
],

'permissions' => ['protected' => []], // your permission names the UI must refuse to delete
'invitation_expiry_hours' => 48,

Every view, layout and route name is overridable.

Features

  • Users — CRUD, soft deletes, active/inactive, enforce_2fa per user
  • Invitations — invite by email, user sets their own password at /set-password/{token}; tokens are single-use, expire after invitation_expiry_hours, and the route is throttled
  • Roles & permissions — full runtime CRUD; permissions generated from your policies
  • Auth — Fortify-backed 2FA (TOTP + recovery codes), passkeys, password reset, email verification, with dedicated rate limiters for login / two-factor / passkeys
  • Login history — IP, user agent, derived browser and platform, optional GeoIP country
  • Impersonation — super admin only, double-gated
  • Avatars — optional, through a resolver you name; the package itself ships no image handling
  • Sidebar — one include renders every admin link, this package's and any other package's, grouped under headings and filtered by policy

Commands

php artisan user-management:install

Publishes the config and migrations, and injects <x-user-management::nav /> into your sidebar.

php artisan user-management:super-admin

Creates or promotes a super admin. Prohibited in production.

php artisan user-management:sync-permissions

Generates a permission per policy method and upserts them. Idempotent — safe on every deploy.

Policies are found two ways: everything registered through Gate::policy(), which is what makes a policy shipped inside another package discoverable at all, and any policy file under policies.paths. A package opts in by declaring PERMISSION_SUBJECT on its policy; policies in your own namespaces are always included. Unrelated dependencies are left alone.

Add --dry-run to see what would change, or --fresh to remove permissions no longer discovered (protected and custom ones are never deleted).

php artisan user-management:avatar-doctor

Says why the avatar field is not rendering. See below.

Architecture

Contracts are split so consumers depend only on what they use:

  • ManagesUsers — the write side (create, update, delete, invitation)
  • QueriesUsers — the read side (pagination, lookups, counts)
  • UserContract — both, for the rare class that genuinely needs it
  • RoleContract — role and permission persistence

All four are bound to the same repository, so you can replace one side without reimplementing the other.

Two guards live in DeleteUserAction rather than UserPolicy — you cannot delete your own account, and you cannot delete the last super admin. That is deliberate: super admins bypass policies via Gate::before, so a policy check would never run for exactly the people able to trigger it.

Testing

vendor/bin/pest

The suite runs against orchestra/testbench on in-memory SQLite. tests/fixtures/views stands in for the host application's layouts, and TestCase::defineRoutes() provides the host routes the package references by configurable name.

License

MIT

Related Packages

rivalex/clearance

A complete admin panel (Livewire 4 + Flux UI) for managing permissions, roles, r...

127 0
adhocrat-io/arkhe-main

Bootstrap a Laravel backend with users, roles and permissions management on Flux...

185 0
marufsharia/hyro

Hyro is a modular Laravel RBAC ecosystem featuring advanced role-permission mana...

16 0