70 lines
3.9 KiB
PHP
70 lines
3.9 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use Closure;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
|
|
|
class TerritorialRequest
|
|
{
|
|
/** @param Closure(Request): Response $next */
|
|
public function handle(Request $request, Closure $next): Response
|
|
{
|
|
$correlation = (string) Str::uuid();
|
|
try {
|
|
abort_unless(config('territorial.enabled'), 404);
|
|
if ($request->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;
|
|
}
|
|
}
|