Skip to main content

Middleware

Custom middleware lives in backend/app/Http/Middleware/. Wiring is done in backend/bootstrap/app.php (Laravel 11+ style — no kernel class).

App-level (every API request)

The API group in bootstrap/app.php prepends:

OrderMiddlewarePurpose
1RecordAuditContextResolves the App\Support\AuditContext singleton, captures actor + IP + UA + request id, makes them visible to the Auditable trait.

Laravel's defaults still apply (SubstituteBindings, throttle on auth routes, JSON validation error handling).

Route-aliased

These are referenced in routes/api.php by alias.

tenant.resolve

App\Http\Middleware\ResolveTenantMembership — runs on every /tenant/{tenant}/... group. It:

  1. Resolves the URL {tenant} (slug or numeric id) into a Tenant model.
  2. Should verify the authenticated user is a member of that tenant ($user->tenants()->where('id', $tenant->id)->exists()). The current implementation skips this membership re-check — see AUDIT.md §1 for the fix.
  3. Sets the active tenant for the request and binds it into the container so policies can read it via app(Tenant::class).

require.permission:<key>

App\Http\Middleware\RequirePermission — gates the route with one of the keys in App\Auth\PermissionRegistry. Returns 403 if the authenticated user lacks the permission for the active tenant. Multiple keys can be ANDed by chaining the middleware.

require.premium:<feature_slug>

App\Http\Middleware\RequirePremium — gates a route on the tenant's plan having the feature unlocked. Reads from tenants.premium_features JSON column. (Audit note: the middleware falls back to the user's current_tenant_id when the route lacks an explicit {tenant} — make this explicit to avoid leakage across tenants.)

auth:sanctum

Standard Laravel auth guard. Used on the owner portal, the caregiver-accept route, and /auth/me.

signed

Standard Laravel signed-URL middleware. Used on /owner/me/pets/{pet}/documents/{document}/download so the URL itself is the authorization (short-lived, generated by PetDocument::generateSignedUrl).

throttle:<max>,<minutes>

Standard rate-limit middleware. Wired today on:

  • /auth/forgot-passwordthrottle:6,1
  • /auth/reset-passwordthrottle:10,1

(Audit recommends extending this to /auth/login, /auth/verify-reset-token, and /p/{slug}.)

Helpers

App\Support\AuditContext

Per-request singleton with:

  • actorUserId() / actorSystemAdminId()
  • tenantId() (default tenant for cross-tenant safe paths)
  • ip(), userAgent(), requestId()

The Auditable trait reads from this when writing audit_logs rows. Background jobs (e.g. notification dispatch) bind their own AuditContext so events get captured even when no HTTP request is in flight.

Container singletons in AppServiceProvider

  • AuditContext::class — request-scoped singleton.
  • NotificationDriverManager::class — singleton so two reminders in one request share the resolved driver.

Removed: SQLite math UDF registration

A previous version of AppServiceProvider registered SQLite UDFs (SIN/COS/ASIN/SQRT/POWER/RADIANS) to support the haversine distance expression in ProviderDirectoryController. The geo filter has been refactored to a two-phase bounding-box pre-filter (SQL) + PHP-side haversine approach, which is portable across MySQL / PostgreSQL / SQLite without UDFs. The provider boot method no longer registers anything beyond the singletons above.