Search by

esanj / managers

esanj

The Manager package is responsible for handling OAuth2-based authentication and managing admin-level permissions.

v1.0.0 2026-09-28 17:27 UTC

This package is auto-updated.

Last update: 2026-09-30 14:16:40 UTC


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 (its ACCOUNTING_BRIDGE_* env keys are listed below).

⚙️ Installation

composer require esanj/managers

Run the installer:

php artisan manager:install

The installer will:

  1. Publish the assets and the config — it asks before overwriting an existing config/esanj/manager.php.
  2. Optionally run php artisan migrate, then import default permissions (manager:permissions-import).
  3. Append any missing .env keys (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_token tells you whether this manager needs to supply their static token in step 3.
  • The access_token is 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:

  1. verifies the token's signature and that the manager is locally active;
  2. asks accounting to refresh the embedded accounting refresh token (grant_type=refresh_token);
  3. 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-Token and X-Manager-Token-Expires-In;
  4. if accounting rejects it (the manager was blocked/restricted there) → responds 401 and 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

  1. Unauthenticated managers are redirected to the Auth Bridge (auth-bridge.redirect).
  2. After OAuth, GET /{auth_prefix}/managers/token either logs them in (no static token needed) or shows the token form.
  3. POST /{auth_prefix}/managers/token validates the static token and logs them into the manager session guard.
  4. POST /{auth_prefix}/managers/logout logs out.

🔒 Access is tied to the accounting session. On every web request, manager.auth:web checks 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 the manager guard, clears the session token, and sends them back to auth-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 admin can hand out the admin role 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 is 403.

🛡️ 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.