File: /home/xedaptot/ai.naniguide.com/bootstrap/app.php
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) {
$middleware->use([
\Illuminate\Foundation\Http\Middleware\InvokeDeferredCallbacks::class,
// \Illuminate\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Http\Middleware\ValidatePostSize::class,
\Illuminate\Foundation\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
]);
$middleware->appendToGroup('web', [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
// \App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
]);
$middleware->prependToGroup('web_nocsrf', [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
#\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
]);
$middleware->prependToGroup('api', [
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
]);
$middleware->alias([
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
// `frontend` is intentionally NOT aliased here — it is defined
// as a middleware GROUP below so that FrontendRouteGuard runs
// immediately after Frontend on every customer route, without
// having to touch routes/refactor.php.
'backend' => \App\Http\Middleware\Backend::class,
'installed' => \App\Http\Middleware\Installed::class,
'not_installed' => \App\Http\Middleware\NotInstalled::class,
'not_logged_in' => \App\Http\Middleware\NotLoggedIn::class,
'subscription' => \App\Http\Middleware\Subscription::class,
'email_verify' => \App\Http\Middleware\EmailVerify::class,
'2fa' => \App\Http\Middleware\TwoFA::class,
'setdb' => \App\Http\Middleware\SetUserDbConnection::class,
'api_key_auth' => \App\Http\Middleware\ApiKeyAuth::class,
'demo_guard' => \App\Http\Middleware\DemoGuard::class,
// `ai.active` middleware alias now lives in the acelle/ai
// plugin's ServiceProvider (W4 of the AI-extraction plan).
// The earlier `ai_module_visible` env-kill-switch alias was
// removed once the plugin lifecycle owned the install-time
// gate. Removing the plugin folder unbinds the alias
// automatically — no shim needed here.
]);
// `frontend` middleware GROUP — applied to every customer-facing
// route in routes/refactor.php. Composed of:
// 1. Frontend — gates customer_access permission + locale
// 2. FrontendRouteGuard — delegates routing decisions to
// ModuleManager::resolveRouteDecision()
// When no plugin has overridden the slot, ModuleManager seeds a
// null-returning default lazily, so Acelle behaves unchanged with
// no frontend-skin plugin installed. Admin routes use the `backend`
// alias and are unaffected by this group.
$middleware->group('frontend', [
\App\Http\Middleware\Frontend::class,
\App\Http\Middleware\FrontendRouteGuard::class,
]);
$middleware->priority([
\App\Http\Middleware\NotInstalled::class,
\Illuminate\Auth\Middleware\Authenticate::class,
]);
$middleware->validateCsrfTokens(except: [
'webhook/*',
'webhooks/*',
'plugins/webhooks/*',
'delivery/*',
'api/*',
'*/embedded-form-*',
'payments/stripe/credit-card*',
'frontend/*',
'paytr/*',
// RFC 8058 one-click unsubscribe — POST from mail clients (Gmail/Apple)
// has no session cookie and cannot acquire a CSRF token; the signed
// URL is the integrity check.
'c/*/unsubscribe-oneclick/*',
// Messenger W3 public opt-in confirm POSTs (acelle/messenger plugin).
// Per DESIGN.md §9.5: subscribers reach these pages via signed URL
// from the subscribe form submit handler, without a session/cookie.
// The signed URL is the integrity check; CSRF token cannot be
// acquired without auth chrome.
'c/opt-in/sms-confirm/*',
'c/opt-in/sms-resend/*',
]);
})
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
// Laravel 12 registers its base EventServiceProvider in addition to the
// app-level one, and the base instance has `shouldDiscoverEvents()` returning
// true (for its own class). That auto-scans `app/Listeners/` and registers
// every `handle(EventClass $e)` as `Class@handle`, which DUPLICATES our
// explicit `$listen` mappings (verified live: each `BounceReceived` was
// dispatching two `RecordBounce` jobs). Disable discovery so our `$listen`
// array in App\Providers\EventServiceProvider is the single source of truth.
->withEvents(discover: false)
->withExceptions(function (Exceptions $exceptions) {
// Auth — JSON requests get the Laravel-default `message` shape (which
// is what the framework was already emitting anyway when this hook
// was empty). Web requests redirect to admin or customer login based
// on path. This replaces the dead app/Exceptions/Handler.php that
// never auto-registered in Laravel 11+.
$exceptions->render(function (\Illuminate\Auth\AuthenticationException $e, $request) {
if ($request->expectsJson()) {
return response()->json(['message' => 'Unauthenticated.'], 401);
}
$isAdmin = $request->is('admin/*') || $request->is('admin')
|| $request->is('rui/admin/*') || $request->is('rui/admin');
return redirect()->guest($isAdmin ? route('refactor.admin.login') : route('login'));
});
// Mirror CLI exceptions to the admin notification inbox via the
// shared 'admins' broadcast (migrated from the old Handler::report()).
$exceptions->report(function (\Throwable $e) {
if (\App::runningInConsole() && function_exists('isInitiated') && isInitiated()) {
try {
app(\App\Services\Notifications\Notifier::class)->shareWithAdmins(
new \App\Notifications\System\BackendErrorOccurred($e)
);
} catch (\PDOException $dbErr) {
// DB unreachable — log and continue. HARD RULE: do not
// swallow silently. Re-throwing would loop since we're
// already inside the global reporter.
\Log::warning('Notifier dispatch failed during exception report (DB unreachable)', [
'original_exception' => get_class($e),
'original_message' => $e->getMessage(),
'dispatch_error' => $dbErr->getMessage(),
]);
}
}
});
$exceptions->dontReport([
// Currently, Acelle allows user to "cancel" a job by deleting its
// monitor model, resulting in a ModelNotFoundException. Suppress
// here to avoid a log recorded to admin notification area.
\Illuminate\Database\Eloquent\ModelNotFoundException::class,
// For this type of issue, it is normally because the related
// process is terminated by the OS and the error is already logged
// to the related campaign, verification process, etc.
\Illuminate\Queue\MaxAttemptsExceededException::class,
]);
$exceptions->dontFlash([
'current_password',
'password',
'password_confirmation',
]);
})->create();