mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-12 06:48:55 +00:00
6086821d6c
#1680 fixed the redirect rendered from an exception and said plainly what it did not cover: EnsureSetupIsComplete, EnsureAccountIsActive and EnforceTwoFactor answer before HandleInertiaRequests is ever entered, so a response they return never unwinds through Inertia's 302 to 303 upgrade either. Same 405, reached a different way — an account deactivated while its owner was part-way through a form, or one being made to enrol in two-factor. The rule now lives in one place rather than four. Three copies of "if the method is PUT, PATCH or DELETE" is how the fourth caller gets it wrong, and WriteSafeRedirect can carry the explanation of why 303 — which is worth more than the three lines it replaces, because nothing about a bare setStatusCode call says what a browser does with a 302. PUT /timezone is the route the setup test uses: it is one of only two writes a guest can reach and the only one that middleware does not exempt, so the case is real rather than defensive. All three new tests were run against the unfixed middleware and fail there. Extends the work of @denkfabrik-li, who found the gap and wrote it down.
129 lines
6.3 KiB
PHP
129 lines
6.3 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 App\Support\WriteSafeRedirect;
|
|
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. WriteSafeRedirect explains why that matters and holds the
|
|
// rule; the same three middleware that answer before Inertia's is
|
|
// reached apply it too.
|
|
$exceptions->respond(
|
|
fn (SymfonyResponse $response, Throwable $e, Request $request): SymfonyResponse => WriteSafeRedirect::apply($request, $response)
|
|
);
|
|
})->create();
|