kreetancraft/laravel-user-management
| 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 |
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, butlivewire/fluxmust be installed.
Requirements
- PHP
^8.2, Laravel^12|^13 livewire/livewire ^4,livewire/flux ^2,laravel/fortify ^1.37spatie/laravel-permission ^8,spatie/laravel-data ^4,spatie/laravel-query-builder ^7lorisleiva/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 fromuser-management-viewson 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_2faper user - Invitations — invite by email, user sets their own password at
/set-password/{token}; tokens are single-use, expire afterinvitation_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 itRoleContract— 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
A complete admin panel (Livewire 4 + Flux UI) for managing permissions, roles, r...
Bootstrap a Laravel backend with users, roles and permissions management on Flux...
Production-ready Laravel starter kit with a full admin panel, role-based access...
Hyro is a modular Laravel RBAC ecosystem featuring advanced role-permission mana...