webx-ui / module-auth
Administrators, roles and sign-in for a WebX UI admin panel — kept separate from the site's own users.
Requires
- php: ^8.4
- illuminate/auth: ^13.0
- illuminate/console: ^13.0
- illuminate/contracts: ^13.0
- illuminate/database: ^13.0
- illuminate/http: ^13.0
- illuminate/routing: ^13.0
- illuminate/support: ^13.0
- illuminate/validation: ^13.0
- laravel/passport: ^13.8
- webx-ui/localization: ^0.86.0
- webx-ui/mcp: ^0.86.0
- webx-ui/module-admin: ^0.86.0
Requires (Dev)
- orchestra/testbench: ^11.0
- phpunit/phpunit: ^12.0 || ^13.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v0.86.0
- v0.85.0
- v0.84.0
- v0.83.0
- v0.82.0
- v0.81.0
- v0.80.0
- v0.79.1
- v0.79.0
- v0.78.0
- v0.77.0
- v0.76.0
- v0.75.0
- v0.74.0
- v0.73.0
- v0.72.0
- v0.71.0
- v0.70.0
- v0.69.0
- v0.68.0
- v0.67.0
- v0.66.0
- v0.65.0
- v0.64.0
- v0.63.0
- v0.62.1
- v0.62.0
- v0.61.1
- v0.61.0
- v0.60.0
- v0.59.0
- v0.58.0
- v0.57.0
- v0.56.0
- v0.55.0
- v0.54.0
- v0.53.1
- v0.53.0
- v0.52.0
- v0.51.0
- v0.50.0
- v0.49.2
- v0.49.1
- v0.49.0
- v0.48.0
- v0.47.0
- v0.46.0
- v0.45.0
- v0.44.1
- v0.44.0
- v0.43.0
- v0.42.3
- v0.42.2
- v0.42.1
- v0.42.0
- v0.41.0
- v0.40.1
- v0.40.0
- v0.39.0
- v0.38.0
- v0.37.0
- v0.36.0
- v0.35.1
- v0.35.0
- v0.34.0
- v0.33.0
- v0.32.1
- v0.32.0
- v0.31.0
- v0.30.0
- v0.29.1
- v0.29.0
- v0.28.0
- v0.27.2
- v0.27.1
- v0.27.0
- v0.26.1
- v0.26.0
- v0.25.1
- v0.25.0
- v0.24.1
- v0.23.0
- v0.22.1
- v0.22.0
- v0.21.0
- v0.20.1
- v0.20.0
- v0.19.0
- v0.18.0
- v0.17.0
- v0.16.2
- v0.16.1
- v0.16.0
- v0.15.0
- v0.14.1
- v0.14.0
- v0.13.0
- v0.12.1
- v0.12.0
- v0.11.0
- v0.10.0
- v0.9.0
- v0.8.0
- v0.7.0
- v0.6.0
- v0.5.0
- v0.4.0
- v0.3.1
- v0.3.0
- dev-next
This package is auto-updated.
Last update: 2026-10-23 15:16:00 UTC
README
Administrators, roles and sign-in for a WebX UI admin panel.
Installing it is what closes the panel. Before it, webx-ui/module-admin serves its API to anyone who
asks — and says so during webx:install, because that is the kind of thing that is easy to
leave for later.
Requirements
- PHP 8.4+
- Laravel 13
webx-ui/module-admin,webx-ui/mcp
Install
composer require webx-ui/module-auth php artisan migrate php artisan webx:admin
webx:admin asks for a name, an address and a password. There is deliberately no --password
option: a password on the command line lands in the shell history and in the process list. The
first account created is a super administrator, because otherwise nobody could grant anybody
else anything.
Administrators are not users
They live in cms_users, behind their own cms guard, and they are not the people who use the
site. A leak in the public part therefore does not reach the panel, and the two sets of accounts
never have to agree about what an account is.
The guard and provider are registered for you. Anything the application has already defined under those names wins — it knows something this package does not.
Roles and permissions
Permissions are the strings modules declare in their manifest — pages.view, media.manage —
so the manifest is the list of what exists. A role only records which of them it grants, in a
JSON column; a permissions table would be a second copy of that list, quietly diverging.
$role = Role::create([ 'slug' => 'editor', 'name' => 'Editor', 'permissions' => ['pages.view', 'pages.manage'], ]); $user->roles()->attach($role); $user->hasPermission('pages.manage'); // true $user->permissions(); // flattened, deduplicated, sorted
A super administrator passes every check without holding any permission.
Ready-made roles
WebxUi\Auth\Access\DefaultRoles holds six — editor, seo, leads, catalog, developer, viewer —
each made only of what the installed modules declare. The first webx:admin creates them; a
module installed later is topped up into them by every webx:boot, by webx:module:add and by
webx:roles:defaults:
php artisan webx:roles:defaults # create the missing ones, top up the rest php artisan webx:roles:defaults --top-up # only top up — what deploys run php artisan webx:roles:defaults --dry-run # role → permissions it would gain, nothing written
A ready-made role remembers what it has been offered (cms_roles.offered), and a top-up adds
only what was never offered: a permission you took away from it stays away. A role whose
offered is null is the site's own and is never touched, whatever its code. Deploys never
create a missing role, because it may be missing because you deleted it. webx:doctor warns
when a ready-made role has not been offered the permissions of a module installed after it.
On your module's routes:
Route::middleware(['cms.auth', 'cms.can:pages.manage'])->group(/* … */);
Several permissions mean any of them: cms.can:pages.manage,pages.publish.
Signing in
| Route | What it does |
|---|---|
POST /api/cms/auth/login |
Session sign-in. Throttled. |
POST /api/cms/auth/logout |
Ends it. |
GET /api/cms/auth/me |
Who is signed in, with roles and permissions. |
PUT /api/cms/auth/me |
Your own profile: name, photo, language, password with the current one. Any other field is a 422 under its name. |
POST /api/cms/auth/me/email |
A new sign-in address, with the current password: sends a letter with a signed link, changes nothing yet. |
GET /api/cms/auth/me/email/confirm |
The link in that letter. Moves the address once, then lands in the panel (?email-change=done|expired|taken). |
The session is what authenticates the panel, so its API runs through the web group rather
than the stateless api one. A stranger gets 401 rather than a redirect: there is no
server-rendered login page to redirect to, and the front end is waiting for that answer to draw
its own.
A wrong password, an address nobody owns and a deactivated account all fail the same way. A different answer to any of them would turn the login form into a way to find out who has an account. An account switched off mid-session stops working at its next request, not at its next sign-in.
The shell at /cms stays public: it carries no data, and it is what draws the login form.
A new address is offered only where a letter can leave the site: any mail transport but log and
array, or a local machine. webx-auth.email_change (WEBX_AUTH_EMAIL_CHANGE) settles it either
way; me says which as emailChange. Without mail the profile says that whoever manages
administrators changes the address.
What gets written down
Every attempt, successful or not, lands in cms_login_records with the address, the IP and the
user agent. Failures matter more than successes here — a burst of them against one address, or
one address against many, is the shape of an attack.
Events: AdminLoggedIn, AdminLoggedOut, AdminLoginFailed.
For AI agents
The module offers MCP tools for reading who has what, moving people between roles, and looking at the sign-in trail:
admins_list_admins admins:read
admins_list_roles admins:read
admins_list_permissions admins:read
admins_create_role admins:write
admins_update_role admins:write
admins_sync_default_roles admins:write
admins_grant_role admins:write
admins_revoke_role admins:write
admins_recent_sign_ins admins:audit
admins_failed_sign_in_bursts admins:audit
There is deliberately no tool that sets a password, mints a token, creates an account or deletes one. Those are the operations where a mistaken tool call is not a mistake you can review afterwards. A test asserts that no such tool appears.
Every tool that writes honours dry_run, so an agent can be asked what it would change first.
admins_sync_default_roles is webx:roles:defaults under the screen's rules: a role gains only
what the caller holds, and a role refused is named in refused and left as it is.
What agents did
Every tool call an agent makes lands in mcp_calls — written by webx-ui/mcp, around every
call, whichever way it went. This module shows it as the Agent calls view of the
administrators section, behind admins.audit: who the agent acted as, on which connection,
which tool, with what, whether it was answered or refused and why, and how long it took. The
view narrows by administrator, by tool and by outcome, and the choices on offer are the ones
that actually appear in the log.
GET /api/cms/auth/mcp-calls answers it, paginated newest first, with user, tool
(pages_create) and outcome (ok, failed, dry) as query parameters; user=none is the
stdio server, where there was nobody to act as. Retention is webx-mcp.calls.days.
Connections
An agent's access refreshes itself for a month, so a connection lives long enough to be worth
seeing and ending. GET /api/cms/auth/connections lists the ones this person made;
?all=1 lists everybody's and needs admins.manage. DELETE /api/cms/auth/connections/{id}
ends one — your own always, somebody else's with admins.manage — and revokes the refresh
token as well as the access token, without which the connection would outlive the decision by
a month. The row is kept, greyed in the list: the call log points at it.
The panel shows everybody's as the Connections view of the administrators section, and a person's own at the foot of the connect page.
Connect an agent
A second section, connect, in the system group and behind no permission at all: the address
of this panel for agents, a button that copies it, three steps for each client, and one-click
links for Cursor and VS Code. The address carries no secret — that is the point of the OAuth
path — so it is printable, readable aloud and safe to leave on a page.
It is registered only where there is a door to connect to: Passport installed and
webx-mcp.path not false. The address it prints is url(webx-mcp.path), absolute, because
it is pasted into a program on another machine.
Configuration
config/webx-auth.php. If you keep your own middleware stack on the panel's API, set
protect_panel to false and add cms.auth yourself — otherwise this package replaces it.
Not here yet
Password reset, two-factor, and invitations.
An administrator is also the account an agent acts as, so the model carries Passport's
HasApiTokens and Passport is a dependency. It stays inert until a site publishes Passport's
migrations and generates its keys — see webx-ui/mcp for what that opens.
Languages
Ten shipped: en, ru, uk, de, pl, fr, es, it, pt, tr. English is the fallback, and it is laid under the chosen language line by line, so a half-translated group shows what it has and English for the rest.
Only English, Russian and Ukrainian have been read by a speaker; the other seven are machine
translations. A correction from someone who speaks the language is welcome — they are plain PHP
arrays in lang/.
Licence
MIT.