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.3
- 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.41.0
- webx-ui/mcp: ^0.41.0
- webx-ui/module-admin: ^0.41.0
Requires (Dev)
- orchestra/testbench: ^11.0
- phpunit/phpunit: ^12.0 || ^13.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- 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-09-24 18:48:42 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.3+
- 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.
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. |
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.
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_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.
Both role tools honour dry_run, so an agent can be asked what it would change first.
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.