mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-18 17:45:09 +00:00
4737849ec5
A redirect born in exception handling - the guest redirect after an expired login, above all - never travels back through the middleware stack, so Inertia's usual 302-to-303 upgrade cannot reach it. Browsers follow a 302 by replaying the request method on the redirect target (only POST is downgraded to GET), so a widget save whose session just died replays as PUT /login and fails with a 405 that hides the real "please sign in again" (#1673). Repeat the upgrade in the exception pipeline: any 302 answered to a PUT, PATCH or DELETE becomes a 303. Reads keep their 302, POST needs nothing - browsers already downgrade it.
135 lines
6.7 KiB
PHP
135 lines
6.7 KiB
PHP
<?php
|
|
|
|
use App\Http\Middleware\HandleInertiaRequests;
|
|
use App\Http\Middleware\ValidateCsrfToken;
|
|
use App\Modules\Api\Http\Middleware\EnsureApiAccountIsActive;
|
|
use App\Modules\Api\Http\Middleware\EnsureStaffToken;
|
|
use App\Modules\Api\Http\Middleware\EnsureTokenCan;
|
|
use App\Modules\Api\Http\Middleware\RecordApiRequest;
|
|
use App\Modules\Api\Http\Middleware\SetApiLocale;
|
|
use App\Modules\Api\Support\ProblemDetails;
|
|
use App\Modules\Identity\Http\Middleware\EnforceTwoFactor;
|
|
use App\Modules\Identity\Http\Middleware\EnsureAccountIsActive;
|
|
use App\Modules\Identity\Http\Middleware\EnsureSetupIsComplete;
|
|
use App\Modules\Identity\Http\Middleware\EnsureStaff;
|
|
use App\Modules\Platform\Http\Middleware\EnsureCapability;
|
|
use App\Modules\Platform\Http\Middleware\SetLocale;
|
|
use Illuminate\Foundation\Application;
|
|
use Illuminate\Foundation\Configuration\Exceptions;
|
|
use Illuminate\Foundation\Configuration\Middleware;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Session\Middleware\AuthenticateSession;
|
|
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
|
|
|
|
return Application::configure(basePath: dirname(__DIR__))
|
|
->withRouting(
|
|
web: __DIR__.'/../routes/web.php',
|
|
// Versioned at the prefix, not with a header or a query parameter:
|
|
// /api/v1 is a frozen contract, and a future /api/v2 gets its own
|
|
// route file rather than branching inside these controllers.
|
|
api: __DIR__.'/../routes/api.php',
|
|
apiPrefix: 'api/v1',
|
|
commands: __DIR__.'/../routes/console.php',
|
|
health: '/up',
|
|
)
|
|
->withMiddleware(function (Middleware $middleware) {
|
|
// Trusted proxies are configured in config/trustedproxy.php, NOT
|
|
// here. This closure runs when the HTTP kernel is resolved, which is
|
|
// before the dotenv bootstrapper has read .env, so env() returns null
|
|
// here for anything that is not already a real environment variable —
|
|
// silently, and only on web requests (artisan bootstraps in the other
|
|
// order, so a CLI check reports the setting as working). The framework's
|
|
// TrustProxies middleware is in the global stack either way and falls
|
|
// back to that config key on its own.
|
|
|
|
$middleware->web(append: [
|
|
// Binds every session to the password hash it was created under,
|
|
// so changing a password (or a reset) actually terminates the
|
|
// account's other sessions instead of leaving a stolen one live.
|
|
// Required for Auth::logoutOtherDevices() to have any effect.
|
|
AuthenticateSession::class,
|
|
EnsureSetupIsComplete::class,
|
|
EnsureAccountIsActive::class,
|
|
EnforceTwoFactor::class,
|
|
SetLocale::class,
|
|
HandleInertiaRequests::class,
|
|
// Deliberately NOT here: AddLinkHeadersForPreloadedAssets. It
|
|
// copies every Vite preload into a `Link:` response header,
|
|
// and the head of the document already carries the identical
|
|
// tags — twenty of them on the login page, more on a heavier
|
|
// one. The copy is what a browser never reads and a proxy has
|
|
// to buffer: it pushed /files past 6 KB of headers, where the
|
|
// 4 KB proxy_buffer_size that nginx, and therefore Nginx Proxy
|
|
// Manager, defaults to answers 502. Some pages fit and some do
|
|
// not, so it reads as an intermittent fault rather than a
|
|
// header that is always too big (#1664). Nothing is lost but
|
|
// 103 Early Hints, which this application does not send.
|
|
]);
|
|
|
|
// The API group gets none of the web stack above — no session, no
|
|
// CSRF, no Inertia. Locale is the one thing worth carrying over,
|
|
// since validation messages are written for a human to read; the
|
|
// web SetLocale can't be reused because it reads the session.
|
|
$middleware->api(append: [
|
|
SetApiLocale::class,
|
|
// Applied to the group rather than per route, so an endpoint
|
|
// added later is measured without anyone opting in.
|
|
RecordApiRequest::class,
|
|
]);
|
|
|
|
$middleware->throttleApi();
|
|
|
|
$middleware->validateCsrfTokens(except: [
|
|
'uploads/*/parts/*',
|
|
]);
|
|
|
|
// Swapped for the subclass only to name the CSRF cookie after this
|
|
// installation rather than after the framework — see that class for
|
|
// what sharing `XSRF-TOKEN` with a neighbouring app does.
|
|
//
|
|
// `web(replace:)` rather than the bare `replace()`: the latter only
|
|
// reaches the global stack, and CSRF lives in the web group, so it
|
|
// silently does nothing here.
|
|
$middleware->web(replace: [
|
|
Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class => ValidateCsrfToken::class,
|
|
]);
|
|
|
|
$middleware->alias([
|
|
'capability' => EnsureCapability::class,
|
|
'staff' => EnsureStaff::class,
|
|
'staff-token' => EnsureStaffToken::class,
|
|
'token-can' => EnsureTokenCan::class,
|
|
'api-active' => EnsureApiAccountIsActive::class,
|
|
]);
|
|
})
|
|
->withExceptions(function (Exceptions $exceptions) {
|
|
// RFC 7807 for /api/* only. Everything else — web pages, Inertia
|
|
// requests, the public share links — keeps Laravel's own handling
|
|
// untouched, which is why this is scoped by path rather than by
|
|
// whether the request happens to accept JSON (Inertia requests do).
|
|
$exceptions->render(function (Throwable $e, Request $request) {
|
|
$problems = app(ProblemDetails::class);
|
|
|
|
return $problems->shouldHandle($request)
|
|
? $problems->render($request, $e)
|
|
: null;
|
|
});
|
|
|
|
// A redirect born in exception handling — the guest redirect after
|
|
// an expired login, above all — never travels back through the
|
|
// middleware stack, so Inertia's usual 302→303 upgrade cannot reach
|
|
// it. Browsers follow a 302 by replaying the request method on the
|
|
// redirect target (only POST is downgraded to GET), so a PUT that
|
|
// should land on the login page replays as PUT /login and dies with
|
|
// a 405 that hides the real "please sign in again". 303 makes the
|
|
// follow-up a GET, which is the only sensible thing after a write.
|
|
$exceptions->respond(function (SymfonyResponse $response, Throwable $e, Request $request) {
|
|
if ($response->getStatusCode() === SymfonyResponse::HTTP_FOUND
|
|
&& in_array($request->method(), ['PUT', 'PATCH', 'DELETE'], true)) {
|
|
$response->setStatusCode(SymfonyResponse::HTTP_SEE_OTHER);
|
|
}
|
|
|
|
return $response;
|
|
});
|
|
})->create();
|