esanj / managers
The Manager package is responsible for handling OAuth2-based authentication and managing admin-level permissions.
Requires
- php: ^8.2|^8.3|^8.4
- esanj/auth-bridge: >=1.0.0 <2.0.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A microservice‑friendly Laravel package that adds a secure, configurable Manager (admin) panel with OAuth login delegated to the Esanj Auth Bridge, static/expirable access tokens, and granular, role‑based permissions.
Supports: Laravel 12 · 13 — PHP 8.2+
🧠 Overview
esanj/managers plugs a complete Manager subsystem into your Laravel app:
- 👤 Manager accounts with roles (
admin,manager,operator) and per‑manager permissions stored in the DB. - 🔐 Login via the Auth Bridge (OAuth), with an optional per‑manager static token as a second factor.
- 🪪 Stateless API access tokens (HMAC‑signed) for the JSON API, plus session login for the web panel.
- 🧱 A ready‑made web panel (list / create / edit / activity log) and a matching JSON API.
- 🚦 Built‑in caching, rate‑limiting, configurable route prefixes, and activity logging.
ℹ️ This package depends on
esanj/auth-bridge, which performs the actual OAuth handshake. Configure it as well (itsACCOUNTING_BRIDGE_*env keys are listed below).
⚙️ Installation
composer require esanj/managers
Run the installer:
php artisan manager:install
The installer will:
- Publish the assets and the config — it asks before overwriting an existing
config/esanj/manager.php. - Optionally run
php artisan migrate, then import default permissions (manager:permissions-import). - Append any missing
.envkeys (listed below) so you can fill them in.
Then create your first admin:
php artisan manager:create {esanj_id}
This prints a one‑time static token — copy it; it's stored hashed and can't be shown again.
🔐 Environment Variables
The installer ensures these keys exist in .env:
| Key | Purpose |
|---|---|
ACCOUNTING_BRIDGE_CLIENT_ID |
OAuth client ID (used by esanj/auth-bridge). |
ACCOUNTING_BRIDGE_CLIENT_SECRET |
OAuth client secret. |
ACCOUNTING_BRIDGE_BASE_URL |
Base URL of the Auth Bridge / Accounting service. |
ACCOUNTING_BRIDGE_SUCCESS_REDIRECT |
Where the Bridge redirects after a successful OAuth login. |
MANAGER_SUCCESS_REDIRECT |
Where to send a manager after a successful panel login. |
MANAGER_ACCESS_DENIED_REDIRECT |
Where to send a manager who lacks permission. |
MANAGER_PANEL_ROUTE_PREFIX |
Prefix for the web panel routes (default: admin). |
MANAGER_API_ROUTE_PREFIX |
Prefix for the API routes (default: api). |
Other optional keys (all have sensible defaults — see Configuration):
MANAGER_AUTH_ROUTE_PREFIX, MANAGER_LOGO_PATH, MANAGER_ACCESS_TOKEN_TTL, MANAGER_TOKEN_LENGTH,
MANAGER_JUST_API, MANAGER_CACHE_ENABLED, MANAGER_CACHE_STORE, MANAGER_CACHE_PREFIX, MANAGER_CACHE_TTL,
MANAGER_RATE_LIMIT_ENABLED, MANAGER_RATE_LIMIT_MAX_ATTEMPTS, MANAGER_RATE_LIMIT_DECAY_SECONDS.
⚙️ Configuration
The config file is src/config/manager.php, merged under the key esanj.manager and publishable to
config/esanj/manager.php:
php artisan vendor:publish --tag=esanj-manager-config
| Key | Default | Description |
|---|---|---|
logo_path |
null |
Logo shown in the panel. |
success_redirect |
/ |
Redirect after a successful login. |
access_denied_redirect |
/ |
Redirect when a manager lacks a permission (web). |
access_token_expires_in |
15 (minutes) |
Lifetime of an API access token (short; auto-renewed against accounting). |
token_length |
128 |
Max length accepted for a static token (validation). |
just_api |
false |
API‑only mode — disables the web panel, views, and web routes. |
routes.auth_prefix |
admin |
Prefix for the web auth routes (/token, /logout). |
routes.panel_prefix |
admin |
Prefix for the web panel routes (manager CRUD). |
routes.api_prefix |
api |
Prefix for all API routes. |
middlewares.api |
['api', 'manager.auth:api'] |
Middleware stack for API panel routes. |
middlewares.web |
['web', 'manager.auth:web'] |
Middleware stack for web panel routes. |
cache.is_enabled |
true |
Cache manager lookups. |
cache.prefix |
manager_ |
Cache key prefix. |
cache.driver |
null (the app's store) |
Cache store for manager data (MANAGER_CACHE_STORE). |
cache.ttl |
10080 (7 days, minutes) |
Manager cache TTL. |
rate_limit.is_enabled |
true |
Throttle the auth/login endpoints. |
rate_limit.max_attempts |
10 |
Allowed attempts before 429. |
rate_limit.decay_seconds |
600 |
Window for the attempt counter. |
permissions |
4 manager permissions | Default permissions imported into the DB. |
access_provider |
action → permission map | Which permission each controller action requires. |
extra_field |
[] |
Blade components injected into the edit panel. |
Default permissions
'permissions' => [ 'managers.list' => ['display_name' => 'List Managers', 'description' => '...'], 'managers.create' => ['display_name' => 'Create Manager', 'description' => '...'], 'managers.edit' => ['display_name' => 'Edit Managers', 'description' => '...'], 'managers.delete' => ['display_name' => 'Delete Managers', 'description' => '...'], ],
Imported into the permissions table with php artisan manager:permissions-import.
🧰 Artisan Commands
| Command | Description |
|---|---|
manager:install |
Publishes assets, ensures .env keys, optionally migrates & imports perms. |
manager:create {esanj_id?} |
Creates an admin manager and prints a one‑time static token. |
manager:permissions-import |
Upserts the permissions from config('esanj.manager.permissions') into DB. |
🔐 Authentication Flows
API flow
| Step | Request | Body / Query | Returns |
|---|---|---|---|
| 1 | GET /{api_prefix}/managers/redirect |
— | { data: { redirect_url } } — send the user there. |
| 2 | POST /{api_prefix}/managers/verify |
{ code } |
{ data: { requires_token, auth_code } } |
| 3 | POST /{api_prefix}/managers/authenticate |
{ auth_code, token? } |
{ data: { access_token, token_type, expires_in } } |
| 4 | Call the API with Authorization: Bearer {access_token} |
— | the data (+ a renewed token header when applicable) |
code(step 2) is the authorization code the Auth Bridge gives the user after OAuth login.requires_tokentells you whether this manager needs to supply their statictokenin step 3.- The
access_tokenis a short‑lived HMAC‑signed token (not a JWT). It carries the manager's accounting refresh token, encrypted, so it can be renewed silently — the client never sees or handles a separate refresh token, and there is no refresh route.
Silent, automatic renewal (no route). When you call any protected API with an expired access token, the
manager.auth:api middleware transparently:
- verifies the token's signature and that the manager is locally active;
- asks accounting to refresh the embedded accounting refresh token (
grant_type=refresh_token); - if accounting still accepts it → issues a fresh manager access token, serves the request, and returns the new
token in the response headers
X-Manager-Access-TokenandX-Manager-Token-Expires-In; - if accounting rejects it (the manager was blocked/restricted there) → responds
401and grants no access.
So a manager blocked at accounting loses all API access within one access‑token lifetime (default 15 min), even though their token had not yet expired at block time — once it expires it can no longer be renewed.
The client's only job is to replace its stored token when a response carries
X-Manager-Access-Token. Because accounting rotates refresh tokens, reusing the old (expired) token after a renewal will fail — always adopt the latest one.
Cross‑origin clients: a browser hides these headers from an SPA on another origin unless CORS exposes them — add them to the host's
config/cors.php, or renewal stays invisible and the client is logged out after one token lifetime:'exposed_headers' => ['X-Manager-Access-Token', 'X-Manager-Token-Expires-In'],
Web flow
- Unauthenticated managers are redirected to the Auth Bridge (
auth-bridge.redirect). - After OAuth,
GET /{auth_prefix}/managers/tokeneither logs them in (no static token needed) or shows the token form. POST /{auth_prefix}/managers/tokenvalidates the static token and logs them into themanagersession guard.POST /{auth_prefix}/managers/logoutlogs out.
🔒 Access is tied to the accounting session. On every web request,
manager.auth:webchecks that the accounting (Auth Bridge) token is still alive — the bridge silently refreshes the short‑lived accounting access token behind the scenes. The moment accounting blocks or restricts the manager, that refresh fails; the middleware then logs the manager out of themanagerguard, clears the session token, and sends them back toauth-bridge.redirect. This is how a block at accounting propagates and cuts access here. For API clients the equivalent is the short access‑token lifetime plus re‑authenticating through the bridge once accounting stops issuing tokens.
🌐 API Endpoints
All under the api_prefix (default api) and guarded by manager.auth:api (Bearer token) plus per‑action
permission checks.
| Method | URI | Description | Permission |
|---|---|---|---|
GET |
/{api_prefix}/managers |
Paginated list (supports search, only_trash). |
managers.list |
POST |
/{api_prefix}/managers |
Create a manager (+ sync permissions by key). |
managers.create |
GET |
/{api_prefix}/managers/{manager} |
Show a manager (includes its permissions). |
managers.list |
PUT |
/{api_prefix}/managers/{manager} |
Update a manager (+ sync permissions by key). |
managers.edit |
DELETE |
/{api_prefix}/managers/{manager} |
Soft‑delete a manager. | managers.delete |
POST |
/{api_prefix}/managers/{id}/restore |
Restore a soft‑deleted manager (404 if not found). |
managers.delete |
GET |
/{api_prefix}/managers/{manager}/meta/{key} |
Read one meta value (404 if unset). |
managers.list |
POST |
/{api_prefix}/managers/{manager}/meta |
Create/update a meta key/value. | managers.edit |
GET |
/{api_prefix}/managers/{manager}/activities |
Paginated activity log (supports search). |
managers.list |
GET |
/{api_prefix}/managers/{manager}/activities/{id} |
One activity entry. | managers.list |
With defaults that means
/api/managers,/api/managers/{manager}, etc.
Accounting identity in API requests
The manager bearer carries the Accounting access token encrypted with Laravel's application key, alongside
the encrypted refresh grant. Its expiry is capped by the Accounting token's expiry. After verifying the bearer,
manager.auth:api exposes that identity through Auth Bridge for the downstream request, including remote actor
exchange. It does not write a browser session. The temporary identity is removed even if the route throws.
Clients must adopt X-Manager-Access-Token when the middleware renews a bearer. Renewal also replaces the embedded
Accounting access token. Older bearers with a refresh grant acquire this identity on their next request; older
bearers without one need a fresh login before performing remote writes. An absent API identity never falls back
to another user's browser token. Deploy the updated managers and auth-bridge packages together.
Only an
admincan hand out theadminrole or edit, delete or restore an admin. A non‑admin can grant or revoke only the permissions they hold themselves; the target's other permissions stay as they are. Anything else is403.
🛡️ Middleware & Authorization
The package registers two middleware aliases:
| Alias | Class | Purpose |
|---|---|---|
manager.auth:{web|api} |
CheckAuthManagerMiddleware |
Authenticates the manager (session for web, Bearer for api). |
manager.permission:{key} |
CheckManagerPermissionMiddleware |
Requires the given permission key. |
It also defines a Gate for every permission key, so Blade @can and Gate checks work out of the box:
// Routes Route::middleware(['manager.auth:web', 'manager.permission:managers.edit'])->group(function () { Route::get('/dashboard', [DashboardController::class, 'index']); }); Route::middleware('manager.auth:api')->group(function () { Route::get('/reports', [ReportController::class, 'index']); });
@can('managers.edit') <a href="...">Edit</a> @endcan
Admins (
role = admin) implicitly pass all permission checks.
📚 Documentation
For a complete, beginner‑friendly, step‑by‑step walkthrough — installing, the auth flows explained simply, adding a new permission, protecting your own page, injecting extra fields into the edit panel, API‑only mode, customizing views/translations, and troubleshooting — see docs/GUIDE.md.
📜 License
Released under the MIT License.