@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class EnsureSimulationRecoveryCookie
|
||||
{
|
||||
public const NAME = 'investmoz_simulation_recovery';
|
||||
|
||||
public const DAYS = 30;
|
||||
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$token = $request->cookie(self::NAME);
|
||||
$issued = ! is_string($token) || strlen($token) < 32 || strlen($token) > 256;
|
||||
if (! is_string($token) || strlen($token) < 32 || strlen($token) > 256) {
|
||||
$provided = $request->header('X-Simulation-Token');
|
||||
$token = is_string($provided) && strlen($provided) >= 32 && strlen($provided) <= 256
|
||||
? $provided
|
||||
: Str::random(96);
|
||||
}
|
||||
$request->headers->set('X-Simulation-Token', $token);
|
||||
|
||||
$response = $next($request);
|
||||
$forget = $request->isMethod('DELETE')
|
||||
&& $request->path() === 'api/v1/simulation-drafts/current'
|
||||
&& $response->getStatusCode() === 204;
|
||||
$activity = $response->isSuccessful() && $this->isExplicitDraftActivity($request, $response);
|
||||
$replay = $response->isSuccessful() && $this->isDraftActivityReplay($request, $response);
|
||||
|
||||
if (! $issued && ! $activity && ! $replay && ! $forget) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Successful draft writes reset the server deadline to now + DAYS, so
|
||||
// refresh the cookie by the same duration. Routine reads never refresh it.
|
||||
$minutes = $forget ? 0 : self::DAYS * 24 * 60;
|
||||
if ($replay) {
|
||||
$payload = json_decode((string) $response->getContent(), true);
|
||||
$expiry = is_array($payload) ? ($payload['expires_at'] ?? null) : null;
|
||||
if (is_string($expiry)) {
|
||||
try {
|
||||
$secondsLeft = \Illuminate\Support\Carbon::parse($expiry)->getTimestamp() - now()->getTimestamp();
|
||||
$minutes = max(1, (int) ceil($secondsLeft / 60));
|
||||
} catch (\Throwable) {
|
||||
// Preserve the standard cookie lifetime if the response has no valid deadline.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $response->withCookie(cookie(
|
||||
self::NAME,
|
||||
$forget ? '' : $token,
|
||||
$minutes,
|
||||
'/api/v1',
|
||||
null,
|
||||
$request->isSecure(),
|
||||
true,
|
||||
false,
|
||||
'lax',
|
||||
));
|
||||
}
|
||||
|
||||
private function isExplicitDraftActivity(Request $request, Response $response): bool
|
||||
{
|
||||
if ($request->isMethod('POST') && $request->path() === 'api/v1/simulation-drafts') {
|
||||
return $response->getStatusCode() === 201;
|
||||
}
|
||||
|
||||
return ($request->isMethod('PUT') && preg_match('#^api/v1/simulation-drafts/[0-9a-f-]{36}$#i', $request->path()) === 1)
|
||||
|| ($request->isMethod('POST') && preg_match('#^api/v1/simulation-drafts/[0-9a-f-]{36}/(?:conversions|touch)$#i', $request->path()) === 1);
|
||||
}
|
||||
|
||||
private function isDraftActivityReplay(Request $request, Response $response): bool
|
||||
{
|
||||
if (! $request->isMethod('POST') || $response->getStatusCode() !== 200) {
|
||||
return false;
|
||||
}
|
||||
if ($request->path() !== 'api/v1/simulation-drafts'
|
||||
&& preg_match('#^api/v1/simulation-drafts/[0-9a-f-]{36}/conversions$#i', $request->path()) !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$payload = json_decode((string) $response->getContent(), true);
|
||||
|
||||
return is_array($payload) && ($payload['data']['replayed'] ?? $payload['replayed'] ?? false) === true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user