Files
Investidor-V2/app/Http/Middleware/EnsureSimulationRecoveryCookie.php
T

95 lines
3.6 KiB
PHP

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
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 = 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.
}
}
}
$response->headers->setCookie(cookie(
self::NAME,
$forget ? '' : $token,
$minutes,
'/api/v1',
null,
$request->isSecure(),
true,
false,
'lax',
));
return $response;
}
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;
}
}