isMethodSafe()) { $response = $next($request); } else { abort_unless($request->user()->can('manage-portal') && $request->user()->two_factor_confirmed_at && $request->user()->two_factor_secret, 403); $role = $request->is('*/transitions') ? match ($request->input('action')) { 'approved', 'rejected' => 'reviewers', 'published', 'archived', 'revoked' => 'publishers', default => 'editors', } : 'editors'; abort_unless(in_array(strtolower($request->user()->email), config('territorial.'.$role, []), true), 403, 'Função editorial não atribuída.'); $key = $request->header('Idempotency-Key', ''); abort_unless(Str::isUuid($key), 422, 'Idempotency-Key UUID obrigatório.'); $scope = $request->method().':'.$request->path(); $actor = $request->user()->id; $hash = hash('sha256', json_encode($request->all(), JSON_THROW_ON_ERROR)); $response = DB::transaction(function () use ($request, $next, $key, $scope, $actor, $hash) { DB::select('SELECT pg_advisory_xact_lock(280926, hashtext(?))', [$actor.':'.$key.':'.$scope]); $query = DB::table('geo_write_receipts')->where('actor_id', $actor)->where('idempotency_key', $key)->where('scope', $scope); $existing = $query->first(); if ($existing) { abort_unless(hash_equals($existing->request_hash, $hash), 409, 'Chave reutilizada com conteúdo diferente.'); return response(Crypt::decryptString($existing->response), $existing->http_status, ['Content-Type' => 'application/json']); } $result = $next($request); if ($result->isSuccessful()) { $body = $result->getContent(); if (! is_string($body)) { throw new \RuntimeException('Expected JSON response body.'); } DB::table('geo_write_receipts')->insert(['id' => (string) Str::uuid(), 'actor_id' => $actor, 'idempotency_key' => $key, 'scope' => $scope, 'request_hash' => $hash, 'response' => Crypt::encryptString($body), 'http_status' => $result->getStatusCode()]); } return $result; }); } } catch (ValidationException $exception) { $response = response()->json(['type' => 'about:blank', 'title' => 'Dados inválidos', 'status' => 422, 'errors' => $exception->errors(), 'correlation_id' => $correlation], 422, ['Content-Type' => 'application/problem+json']); } catch (HttpExceptionInterface $exception) { $status = $exception->getStatusCode(); $response = response()->json(['type' => 'about:blank', 'title' => Response::$statusTexts[$status] ?? 'Request failed', 'status' => $status, 'detail' => $exception->getMessage(), 'correlation_id' => $correlation], $status, ['Content-Type' => 'application/problem+json']); } $response->headers->set('X-Correlation-ID', $correlation); $response->headers->set('Cache-Control', 'private, no-store'); return $response; } }